diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index a59fd0ebf8..445a1cf11c 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -11,6 +11,7 @@ jobs: runs-on: ubuntu-20.04 container: image: ghcr.io/commaai/openpilot-base:latest + if: github.repository == 'commaai/openpilot' steps: - uses: actions/checkout@v4 with: @@ -36,6 +37,7 @@ jobs: runs-on: ubuntu-20.04 container: image: ghcr.io/commaai/openpilot-base:latest + if: github.repository == 'commaai/openpilot' steps: - uses: actions/checkout@v4 - name: poetry lock diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 860cfa48f7..d1dff147f2 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -14,7 +14,6 @@ concurrency: env: PYTHONWARNINGS: error BASE_IMAGE: openpilot-base - CL_BASE_IMAGE: openpilot-base-cl AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }} DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} @@ -22,10 +21,6 @@ env: RUN: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PRE_COMMIT_HOME=/tmp/pre-commit -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/pre-commit:/tmp/pre-commit -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c - BUILD_CL: selfdrive/test/docker_build.sh cl - - RUN_CL: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $CL_BASE_IMAGE /bin/bash -c - PYTEST: pytest --continue-on-collection-errors --cov --cov-report=xml --cov-append --durations=0 --durations-min=5 --hypothesis-seed 0 -n logical jobs: @@ -106,11 +101,6 @@ jobs: - uses: ./.github/workflows/setup-with-retry with: docker_hub_pat: ${{ secrets.DOCKER_HUB_PAT }} - - name: Build and push CL Docker image - if: matrix.arch == 'x86_64' - run: | - unset TARGET_ARCHITECTURE - eval "$BUILD_CL" docker_push_multiarch: name: docker push multiarch tag @@ -127,7 +117,7 @@ jobs: - name: Merge x64 and arm64 tags run: | export PUSH_IMAGE=true - selfdrive/test/docker_tag_multiarch.sh base x86_64 aarch64 + scripts/retry.sh selfdrive/test/docker_tag_multiarch.sh base x86_64 aarch64 static_analysis: name: static analysis @@ -258,15 +248,13 @@ jobs: key: regen-${{ hashFiles('.github/workflows/selfdrive_tests.yaml', 'selfdrive/test/process_replay/test_regen.py') }} - name: Build base Docker image run: eval "$BUILD" - - name: Build Docker image - run: eval "$BUILD_CL" - name: Build openpilot run: | ${{ env.RUN }} "scons -j$(nproc)" - name: Run regen timeout-minutes: 30 run: | - ${{ env.RUN_CL }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \ + ${{ env.RUN }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \ chmod -R 777 /tmp/comma_download_cache" test_modeld: @@ -279,9 +267,6 @@ jobs: - uses: ./.github/workflows/setup-with-retry - name: Build base Docker image run: eval "$BUILD" - - name: Build Docker image - # Sim docker is needed to get the OpenCL drivers - run: eval "$BUILD_CL" - name: Build openpilot run: | ${{ env.RUN }} "scons -j$(nproc)" @@ -289,14 +274,14 @@ jobs: - name: Run model replay with ONNX timeout-minutes: 4 run: | - ${{ env.RUN_CL }} "unset PYTHONWARNINGS && \ + ${{ env.RUN }} "unset PYTHONWARNINGS && \ ONNXCPU=1 NO_NAV=1 coverage run selfdrive/test/process_replay/model_replay.py && \ coverage combine && \ coverage xml" - name: Run unit tests timeout-minutes: 4 run: | - ${{ env.RUN_CL }} "unset PYTHONWARNINGS && \ + ${{ env.RUN }} "unset PYTHONWARNINGS && \ $PYTEST selfdrive/modeld" - name: "Upload coverage to Codecov" uses: codecov/codecov-action@v3 @@ -328,7 +313,7 @@ jobs: - name: Build openpilot run: ${{ env.RUN }} "scons -j$(nproc)" - name: Test car models - timeout-minutes: 25 + timeout-minutes: 10 run: | ${{ env.RUN }} "$PYTEST selfdrive/car/tests/test_models.py && \ chmod -R 777 /tmp/comma_download_cache" diff --git a/.github/workflows/tools_tests.yaml b/.github/workflows/tools_tests.yaml index c185fd1d3c..d15ab8cd60 100644 --- a/.github/workflows/tools_tests.yaml +++ b/.github/workflows/tools_tests.yaml @@ -12,17 +12,12 @@ concurrency: env: BASE_IMAGE: openpilot-base - CL_BASE_IMAGE: openpilot-base-cl DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} BUILD: selfdrive/test/docker_build.sh base RUN: docker run --shm-size 1G -v $GITHUB_WORKSPACE:/tmp/openpilot -w /tmp/openpilot -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c - BUILD_CL: selfdrive/test/docker_build.sh cl - - RUN_CL: docker run --shm-size 1G -v $GITHUB_WORKSPACE:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $CL_BASE_IMAGE /bin/bash -c - jobs: plotjuggler: @@ -52,8 +47,6 @@ jobs: with: submodules: true - uses: ./.github/workflows/setup-with-retry - - name: Build base cl image - run: eval "$BUILD_CL" - name: Setup to push to repo if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot' run: | @@ -104,6 +97,6 @@ jobs: timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 10 || 30) }} # allow more time when we missed the scons cache run: ${{ env.RUN }} "scons -j$(nproc)" - name: Test notebooks - timeout-minutes: 2 + timeout-minutes: 3 run: | ${{ env.RUN }} "pip install nbmake && pytest --nbmake tools/car_porting/examples/" \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a336bd3378..335ccae456 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,6 +32,11 @@ repos: # if you've got a short variable name that's getting flagged, add it here - -L bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints - --builtins clear,rare,informal,usage,code,names,en-GB_to_en-US +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.2.2 + hooks: + - id: ruff + exclude: '^(third_party/)|(cereal/)|(panda/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(teleoprtc/)|(teleoprtc_repo/)' - repo: local hooks: - id: mypy @@ -43,11 +48,6 @@ repos: - --local-partial-types - --explicit-package-bases exclude: '^(third_party/)|(cereal/)|(opendbc/)|(panda/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(teleoprtc/)|(teleoprtc_repo/)|(xx/)' -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.0 - hooks: - - id: ruff - exclude: '^(third_party/)|(cereal/)|(panda/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(teleoprtc/)|(teleoprtc_repo/)' - repo: local hooks: - id: cppcheck @@ -74,6 +74,14 @@ repos: # https://google.github.io/styleguide/cppguide.html # relevant rules are whitelisted, see all options with: cpplint --filter= - --filter=-build,-legal,-readability,-runtime,-whitespace,+build/include_subdir,+build/forward_decl,+build/include_what_you_use,+build/deprecated,+whitespace/comma,+whitespace/line_length,+whitespace/empty_if_body,+whitespace/empty_loop_body,+whitespace/empty_conditional_body,+whitespace/forcolon,+whitespace/parens,+whitespace/semicolon,+whitespace/tab,+readability/braces +- repo: https://github.com/MarcoGorelli/cython-lint + rev: v0.16.0 + hooks: + - id: cython-lint + exclude: '^(third_party/)|(cereal/)|(body/)|(rednose/)|(rednose_repo/)|(opendbc/)|(panda/)|(generated/)' + args: + - --max-line-length=240 + - --ignore=E111, E302, E305 - repo: local hooks: - id: test_translations @@ -90,6 +98,6 @@ repos: args: - --lock - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.27.4 + rev: 0.28.0 hooks: - id: check-github-workflows diff --git a/Dockerfile.openpilot_base b/Dockerfile.openpilot_base index d280d2c9ec..a6c4c71790 100644 --- a/Dockerfile.openpilot_base +++ b/Dockerfile.openpilot_base @@ -22,6 +22,44 @@ RUN cd /tmp && \ rm -rf arm/ && \ rm -rf thumb/nofp thumb/v6* thumb/v8* thumb/v7+fp thumb/v7-r+fp.sp +# Add OpenCL +RUN apt-get update && apt-get install -y --no-install-recommends \ + apt-utils \ + alien \ + unzip \ + tar \ + curl \ + xz-utils \ + dbus \ + gcc-arm-none-eabi \ + tmux \ + vim \ + lsb-core \ + libx11-6 \ + && rm -rf /var/lib/apt/lists/* + +ARG INTEL_DRIVER=l_opencl_p_18.1.0.015.tgz +ARG INTEL_DRIVER_URL=https://registrationcenter-download.intel.com/akdlm/irc_nas/vcp/15532 +RUN mkdir -p /tmp/opencl-driver-intel + +RUN cd /tmp/opencl-driver-intel && \ + echo INTEL_DRIVER is $INTEL_DRIVER && \ + curl -O $INTEL_DRIVER_URL/$INTEL_DRIVER && \ + tar -xzf $INTEL_DRIVER && \ + for i in $(basename $INTEL_DRIVER .tgz)/rpm/*.rpm; do alien --to-deb $i; done && \ + dpkg -i *.deb && \ + rm -rf $INTEL_DRIVER $(basename $INTEL_DRIVER .tgz) *.deb && \ + mkdir -p /etc/OpenCL/vendors && \ + echo /opt/intel/opencl_compilers_and_libraries_18.1.0.015/linux/compiler/lib/intel64_lin/libintelocl.so > /etc/OpenCL/vendors/intel.icd && \ + cd / && \ + rm -rf /tmp/opencl-driver-intel + +ENV NVIDIA_VISIBLE_DEVICES all +ENV NVIDIA_DRIVER_CAPABILITIES graphics,utility,compute +ENV QTWEBENGINE_DISABLE_SANDBOX 1 + +RUN dbus-uuidgen > /etc/machine-id + ARG USER=batman ARG USER_UID=1000 RUN useradd -m -s /bin/bash -u $USER_UID $USER diff --git a/Dockerfile.openpilot_base_cl b/Dockerfile.openpilot_base_cl deleted file mode 100644 index 4c8ecfc78d..0000000000 --- a/Dockerfile.openpilot_base_cl +++ /dev/null @@ -1,37 +0,0 @@ -FROM ghcr.io/commaai/openpilot-base:latest - -RUN apt-get update && apt-get install -y --no-install-recommends\ - apt-utils \ - alien \ - unzip \ - tar \ - curl \ - xz-utils \ - dbus \ - gcc-arm-none-eabi \ - tmux \ - vim \ - lsb-core \ - libx11-6 \ - && rm -rf /var/lib/apt/lists/* - -# Intel OpenCL driver -ARG INTEL_DRIVER=l_opencl_p_18.1.0.015.tgz -ARG INTEL_DRIVER_URL=https://registrationcenter-download.intel.com/akdlm/irc_nas/vcp/15532 -RUN mkdir -p /tmp/opencl-driver-intel -WORKDIR /tmp/opencl-driver-intel -RUN echo INTEL_DRIVER is $INTEL_DRIVER && \ - curl -O $INTEL_DRIVER_URL/$INTEL_DRIVER && \ - tar -xzf $INTEL_DRIVER && \ - for i in $(basename $INTEL_DRIVER .tgz)/rpm/*.rpm; do alien --to-deb $i; done && \ - dpkg -i *.deb && \ - rm -rf $INTEL_DRIVER $(basename $INTEL_DRIVER .tgz) *.deb && \ - mkdir -p /etc/OpenCL/vendors && \ - echo /opt/intel/opencl_compilers_and_libraries_18.1.0.015/linux/compiler/lib/intel64_lin/libintelocl.so > /etc/OpenCL/vendors/intel.icd && \ - rm -rf /tmp/opencl-driver-intel -ENV NVIDIA_VISIBLE_DEVICES all -ENV NVIDIA_DRIVER_CAPABILITIES graphics,utility,compute -ENV QTWEBENGINE_DISABLE_SANDBOX 1 - -RUN dbus-uuidgen > /etc/machine-id - diff --git a/Jenkinsfile b/Jenkinsfile index 392253e845..d716510bfe 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -12,10 +12,12 @@ def retryWithDelay(int maxRetries, int delay, Closure body) { def device(String ip, String step_label, String cmd) { withCredentials([file(credentialsId: 'id_rsa', variable: 'key_file')]) { def ssh_cmd = """ -ssh -tt -o ConnectTimeout=30 -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o BatchMode=yes -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' /usr/bin/bash <<'END' +ssh -tt -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=2 -o BatchMode=yes -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' /usr/bin/bash <<'END' set -e +shopt -s huponexit # kill all child processes when the shell exits + export CI=1 export PYTHONWARNINGS=error export LOGPRINT=debug @@ -85,10 +87,6 @@ def deviceStage(String stageName, String deviceType, List extra_env, def steps) device(device_ip, "git checkout", extra + "\n" + readFile("selfdrive/test/setup_device_ci.sh")) } steps.each { item -> - if (branch != "master" && item.size() == 3 && !hasDirectoryChanged(item[2])) { - println "Skipped '${item[0]}', no relevant changes were detected." - return; - } device(device_ip, item[0], item[1]) } } @@ -143,20 +141,6 @@ def setupCredentials() { } } -def hasDirectoryChanged(List paths) { - for (change in currentBuild.changeSets) { - for (item in change.items) { - for (affectedPath in item.affectedPaths) { - for (path in paths) { - if (affectedPath.startsWith(path)) { - return true - } - } - } - } - } - return false -} node { env.CI = "1" @@ -207,7 +191,7 @@ node { 'HW + Unit Tests': { deviceStage("tici-hardware", "tici-common", ["UNSAFE=1"], [ ["build", "cd selfdrive/manager && ./build.py"], - ["test pandad", "pytest selfdrive/boardd/tests/test_pandad.py", ["panda/", "selfdrive/boardd/"]], + ["test pandad", "pytest selfdrive/boardd/tests/test_pandad.py"], ["test power draw", "pytest -s system/hardware/tici/tests/test_power_draw.py"], ["test encoder", "LD_LIBRARY_PATH=/usr/local/lib pytest system/loggerd/tests/test_encoder.py"], ["test pigeond", "pytest system/sensord/tests/test_pigeond.py"], @@ -252,31 +236,13 @@ node { deviceStage("tizi", "tizi", ["UNSAFE=1"], [ ["build openpilot", "cd selfdrive/manager && ./build.py"], ["test boardd loopback", "SINGLE_PANDA=1 pytest selfdrive/boardd/tests/test_boardd_loopback.py"], - ["test pandad", "pytest selfdrive/boardd/tests/test_pandad.py", ["panda/", "selfdrive/boardd/"]], + ["test pandad", "pytest selfdrive/boardd/tests/test_pandad.py"], ["test amp", "pytest system/hardware/tici/tests/test_amplifier.py"], ["test hw", "pytest system/hardware/tici/tests/test_hardware.py"], - ["test qcomgpsd", "pytest system/qcomgpsd/tests/test_qcomgpsd.py", ["system/qcomgpsd/"]], + ["test qcomgpsd", "pytest system/qcomgpsd/tests/test_qcomgpsd.py"], ]) }, - // *** PC tests *** - 'PC tests': { - pcStage("PC tests") { - // tests that our build system's dependencies are configured properly, - // needs a machine with lots of cores - sh label: "test multi-threaded build", - script: '''#!/bin/bash - scons --no-cache --random -j$(nproc)''' - } - }, - 'car tests': { - pcStage("car tests") { - sh label: "build", script: "selfdrive/manager/build.py" - sh label: "run car tests", script: "cd selfdrive/car/tests && MAX_EXAMPLES=300 INTERNAL_SEG_CNT=300 FILEREADER_CACHE=1 \ - INTERNAL_SEG_LIST=selfdrive/car/tests/test_models_segs.txt pytest test_models.py test_car_interfaces.py" - } - }, - ) } } catch (Exception e) { diff --git a/RELEASES.md b/RELEASES.md index aa3a80d56d..d0fd530db1 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,4 +1,9 @@ -Version 0.9.6 (2024-02-XX) +Version 0.9.7 (2024-XX-XX) +======================== +* New driving model +* Support for many hybrid Ford models + +Version 0.9.6 (2024-02-27) ======================== * New driving model * Vision model trained on more data @@ -9,8 +14,12 @@ Version 0.9.6 (2024-02-XX) * AGNOS 9 * comma body streaming and controls over WebRTC * Improved fuzzy fingerprinting for many makes and models +* Alpha longitudinal support for new Toyota models +* Chevrolet Equinox 2019-22 support thanks to JasonJShuler and nworb-cire! +* Dodge Durango 2020-21 support * Hyundai Staria 2023 support thanks to sunnyhaibin! * Kia Niro Plug-in Hybrid 2022 support thanks to sunnyhaibin! +* Lexus LC 2024 support thanks to nelsonjchen! * Toyota RAV4 2023-24 support * Toyota RAV4 Hybrid 2023-24 support diff --git a/cereal b/cereal index 80e1e55f0d..2fba1381f4 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 80e1e55f0dd71cea7f596e8b80c7c33865b689f3 +Subproject commit 2fba1381f40df2654f42a2e4bed869f2b7d01a52 diff --git a/common/file_helpers.py b/common/file_helpers.py index dea298a529..417776b87c 100644 --- a/common/file_helpers.py +++ b/common/file_helpers.py @@ -1,7 +1,6 @@ import os import tempfile import contextlib -from typing import Optional class CallbackReader: @@ -24,7 +23,7 @@ class CallbackReader: @contextlib.contextmanager -def atomic_write_in_dir(path: str, mode: str = 'w', buffering: int = -1, encoding: Optional[str] = None, newline: Optional[str] = None, +def atomic_write_in_dir(path: str, mode: str = 'w', buffering: int = -1, encoding: str | None = None, newline: str | None = None, overwrite: bool = False): """Write to a file atomically using a temporary file in the same directory as the destination file.""" dir_name = os.path.dirname(path) diff --git a/common/gpio.py b/common/gpio.py index 88a9479a60..68932cb87a 100644 --- a/common/gpio.py +++ b/common/gpio.py @@ -1,6 +1,5 @@ import os from functools import lru_cache -from typing import Optional, List def gpio_init(pin: int, output: bool) -> None: try: @@ -16,7 +15,7 @@ def gpio_set(pin: int, high: bool) -> None: except Exception as e: print(f"Failed to set gpio {pin} value: {e}") -def gpio_read(pin: int) -> Optional[bool]: +def gpio_read(pin: int) -> bool | None: val = None try: with open(f"/sys/class/gpio/gpio{pin}/value", 'rb') as f: @@ -37,7 +36,7 @@ def gpio_export(pin: int) -> None: print(f"Failed to export gpio {pin}") @lru_cache(maxsize=None) -def get_irq_action(irq: int) -> List[str]: +def get_irq_action(irq: int) -> list[str]: try: with open(f"/sys/kernel/irq/{irq}/actions") as f: actions = f.read().strip().split(',') @@ -45,7 +44,7 @@ def get_irq_action(irq: int) -> List[str]: except FileNotFoundError: return [] -def get_irqs_for_action(action: str) -> List[str]: +def get_irqs_for_action(action: str) -> list[str]: ret = [] with open("/proc/interrupts") as f: for l in f.readlines(): diff --git a/common/mock/__init__.py b/common/mock/__init__.py index e0dbf45c74..8c86bbd394 100644 --- a/common/mock/__init__.py +++ b/common/mock/__init__.py @@ -6,7 +6,6 @@ example in common/tests/test_mock.py import functools import threading -from typing import List, Union from cereal.messaging import PubMaster from cereal.services import SERVICE_LIST from openpilot.common.mock.generators import generate_liveLocationKalman @@ -18,7 +17,7 @@ MOCK_GENERATOR = { } -def generate_messages_loop(services: List[str], done: threading.Event): +def generate_messages_loop(services: list[str], done: threading.Event): pm = PubMaster(services) rk = Ratekeeper(100) i = 0 @@ -32,7 +31,7 @@ def generate_messages_loop(services: List[str], done: threading.Event): rk.keep_time() -def mock_messages(services: Union[List[str], str]): +def mock_messages(services: list[str] | str): if isinstance(services, str): services = [services] diff --git a/common/params.cc b/common/params.cc index 3ce2505243..eb75705ca3 100644 --- a/common/params.cc +++ b/common/params.cc @@ -125,6 +125,7 @@ std::unordered_map keys = { {"ForcePowerDown", PERSISTENT}, {"GitBranch", PERSISTENT}, {"GitCommit", PERSISTENT}, + {"GitCommitDate", PERSISTENT}, {"GitDiff", PERSISTENT}, {"GithubSshKeys", PERSISTENT}, {"GithubUsername", PERSISTENT}, diff --git a/common/params_pyx.pyx b/common/params_pyx.pyx index 47d2075df2..535514e521 100644 --- a/common/params_pyx.pyx +++ b/common/params_pyx.pyx @@ -29,7 +29,7 @@ cdef extern from "common/params.h": def ensure_bytes(v): - return v.encode() if isinstance(v, str) else v; + return v.encode() if isinstance(v, str) else v class UnknownKeyName(Exception): pass diff --git a/common/prefix.py b/common/prefix.py index d027e3e5a1..40f2f34b74 100644 --- a/common/prefix.py +++ b/common/prefix.py @@ -2,14 +2,13 @@ import os import shutil import uuid -from typing import Optional from openpilot.common.params import Params from openpilot.system.hardware.hw import Paths from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT class OpenpilotPrefix: - def __init__(self, prefix: Optional[str] = None, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): + def __init__(self, prefix: str | None = None, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): self.prefix = prefix if prefix else str(uuid.uuid4().hex[0:15]) self.msgq_path = os.path.join('/dev/shm', self.prefix) self.clean_dirs_on_exit = clean_dirs_on_exit diff --git a/common/realtime.py b/common/realtime.py index a398146166..6b8587ff06 100644 --- a/common/realtime.py +++ b/common/realtime.py @@ -3,7 +3,6 @@ import gc import os import time from collections import deque -from typing import Optional, List, Union from setproctitle import getproctitle @@ -33,12 +32,12 @@ def set_realtime_priority(level: int) -> None: os.sched_setscheduler(0, os.SCHED_FIFO, os.sched_param(level)) -def set_core_affinity(cores: List[int]) -> None: +def set_core_affinity(cores: list[int]) -> None: if not PC: os.sched_setaffinity(0, cores) -def config_realtime_process(cores: Union[int, List[int]], priority: int) -> None: +def config_realtime_process(cores: int | list[int], priority: int) -> None: gc.disable() set_realtime_priority(priority) c = cores if isinstance(cores, list) else [cores, ] @@ -46,7 +45,7 @@ def config_realtime_process(cores: Union[int, List[int]], priority: int) -> None class Ratekeeper: - def __init__(self, rate: float, print_delay_threshold: Optional[float] = 0.0) -> None: + def __init__(self, rate: float, print_delay_threshold: float | None = 0.0) -> None: """Rate in Hz for ratekeeping. print_delay_threshold must be nonnegative.""" self._interval = 1. / rate self._next_frame_time = time.monotonic() + self._interval diff --git a/common/transformations/orientation.py b/common/transformations/orientation.py index ce4378738d..86e6a6c347 100644 --- a/common/transformations/orientation.py +++ b/common/transformations/orientation.py @@ -1,5 +1,5 @@ import numpy as np -from typing import Callable +from collections.abc import Callable from openpilot.common.transformations.transformations import (ecef_euler_from_ned_single, euler2quat_single, diff --git a/common/transformations/transformations.pxd b/common/transformations/transformations.pxd index 7af0098701..964adf06ec 100644 --- a/common/transformations/transformations.pxd +++ b/common/transformations/transformations.pxd @@ -1,4 +1,4 @@ -#cython: language_level=3 +# cython: language_level=3 from libcpp cimport bool cdef extern from "orientation.cc": diff --git a/common/transformations/transformations.pyx b/common/transformations/transformations.pyx index c5cb9e0056..ae045c369d 100644 --- a/common/transformations/transformations.pyx +++ b/common/transformations/transformations.pyx @@ -17,7 +17,6 @@ from openpilot.common.transformations.transformations cimport ecef2geodetic as e from openpilot.common.transformations.transformations cimport LocalCoord_c -import cython import numpy as np cimport numpy as np @@ -34,14 +33,14 @@ cdef Matrix3 numpy2matrix(np.ndarray[double, ndim=2, mode="fortran"] m): return Matrix3(m.data) cdef ECEF list2ecef(ecef): - cdef ECEF e; + cdef ECEF e e.x = ecef[0] e.y = ecef[1] e.z = ecef[2] return e cdef NED list2ned(ned): - cdef NED n; + cdef NED n n.n = ned[0] n.e = ned[1] n.d = ned[2] @@ -61,7 +60,7 @@ def euler2quat_single(euler): def quat2euler_single(quat): cdef Quaternion q = Quaternion(quat[0], quat[1], quat[2], quat[3]) - cdef Vector3 e = quat2euler_c(q); + cdef Vector3 e = quat2euler_c(q) return [e(0), e(1), e(2)] def quat2rot_single(quat): diff --git a/common/version.h b/common/version.h index 787bc897d7..177882e31d 100644 --- a/common/version.h +++ b/common/version.h @@ -1 +1 @@ -#define COMMA_VERSION "0.9.6" +#define COMMA_VERSION "0.9.7" diff --git a/conftest.py b/conftest.py index e215bb8b4c..0d2a4a8fc4 100644 --- a/conftest.py +++ b/conftest.py @@ -1,3 +1,5 @@ +import contextlib +import gc import os import pytest import random @@ -24,46 +26,50 @@ def pytest_runtest_call(item): yield -@pytest.fixture(scope="function", autouse=True) -def openpilot_function_fixture(request): +@contextlib.contextmanager +def clean_env(): starting_env = dict(os.environ) + yield + os.environ.clear() + os.environ.update(starting_env) + +@pytest.fixture(scope="function", autouse=True) +def openpilot_function_fixture(request): random.seed(0) - # setup a clean environment for each test - with OpenpilotPrefix(shared_download_cache=request.node.get_closest_marker("shared_download_cache") is not None) as prefix: - prefix = os.environ["OPENPILOT_PREFIX"] + with clean_env(): + # setup a clean environment for each test + with OpenpilotPrefix(shared_download_cache=request.node.get_closest_marker("shared_download_cache") is not None) as prefix: + prefix = os.environ["OPENPILOT_PREFIX"] - yield + yield - # ensure the test doesn't change the prefix - assert "OPENPILOT_PREFIX" in os.environ and prefix == os.environ["OPENPILOT_PREFIX"] + # ensure the test doesn't change the prefix + assert "OPENPILOT_PREFIX" in os.environ and prefix == os.environ["OPENPILOT_PREFIX"] - os.environ.clear() - os.environ.update(starting_env) + # cleanup any started processes + manager.manager_cleanup() - # cleanup any started processes - manager.manager_cleanup() + # some processes disable gc for performance, re-enable here + if not gc.isenabled(): + gc.enable() + gc.collect() # If you use setUpClass, the environment variables won't be cleared properly, # so we need to hook both the function and class pytest fixtures @pytest.fixture(scope="class", autouse=True) def openpilot_class_fixture(): - starting_env = dict(os.environ) - - yield - - os.environ.clear() - os.environ.update(starting_env) + with clean_env(): + yield -@pytest.fixture(scope="class") -def tici_setup_fixture(): - """Ensure a consistent state for tests on-device""" +@pytest.fixture(scope="function") +def tici_setup_fixture(openpilot_function_fixture): + """Ensure a consistent state for tests on-device. Needs the openpilot function fixture to run first.""" HARDWARE.initialize_hardware() HARDWARE.set_power_save(False) os.system("pkill -9 -f athena") - os.system("rm /dev/shm/*") @pytest.hookimpl(tryfirst=True) diff --git a/docs/CARS.md b/docs/CARS.md index d82cbe2c96..7ad12189cb 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -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. -# 276 Supported Cars +# 288 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:| @@ -23,6 +23,7 @@ A supported vehicle is one that just works when you install a comma device. All |Cadillac|Escalade ESV 2019[4](#footnotes)|Adaptive Cruise Control (ACC) & LKAS|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-II connector
- 1 comma 3X
- 2 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Chevrolet|Volt 2017-18[4](#footnotes)|Adaptive Cruise Control (ACC)|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-II connector
- 1 comma 3X
- 2 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| @@ -33,13 +34,22 @@ A supported vehicle is one that just works when you install a comma device. All |Chrysler|Pacifica Hybrid 2018|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Chrysler|Pacifica Hybrid 2019-23|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None|| +|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Ford|Bronco Sport 2021-22|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Ford|Explorer 2020-23|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Ford|Explorer Hybrid 2020-23|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Ford|Focus 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Ford|Focus Hybrid 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Ford|Kuga 2020-22|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Ford|Kuga Hybrid 2020-22|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Ford|Kuga Plug-in Hybrid 2020-22|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Ford|Maverick 2022|LARIAT Luxury|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Ford|Maverick 2023|Co-Pilot360 Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Ford|Maverick Hybrid 2023|Co-Pilot360 Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Genesis|G70 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Genesis|G70 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Genesis|G80 2017|All|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai J connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| @@ -72,7 +82,7 @@ A supported vehicle is one that just works when you install a comma device. All |Honda|Odyssey 2018-20|Honda Sensing|openpilot|25 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Honda|Passport 2019-23|All|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Honda|Pilot 2016-22|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Honda|Ridgeline 2017-23|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Ridgeline 2017-24|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Azera 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Azera Hybrid 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Hyundai|Azera Hybrid 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| @@ -162,6 +172,7 @@ A supported vehicle is one that just works when you install a comma device. All |Lexus|GS F 2016|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Lexus|IS 2022-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|LC 2024|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Lexus|NX 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Lexus|NX Hybrid 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| @@ -174,7 +185,8 @@ A supported vehicle is one that just works when you install a comma device. All |Lexus|RX Hybrid 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Lexus|RX Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Lexus|UX Hybrid 2019-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Lincoln|Aviator 2020-21|Co-Pilot360 Plus|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lincoln|Aviator 2020-23|Co-Pilot360 Plus|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lincoln|Aviator Plug-in Hybrid 2020-23|Co-Pilot360 Plus|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |MAN|eTGE 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,13](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 J533 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |MAN|TGE 2017-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,13](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 J533 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Mazda|CX-5 2022-24|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| @@ -213,9 +225,9 @@ A supported vehicle is one that just works when you install a comma device. All |Toyota|Avalon Hybrid 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Toyota|Avalon Hybrid 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Toyota|C-HR 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Toyota|C-HR 2021|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|C-HR 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Toyota|C-HR Hybrid 2021-22|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|C-HR Hybrid 2021-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Toyota|Camry 2018-20|All|Stock|0 mph[9](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Toyota|Camry 2021-24|All|openpilot|0 mph[9](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| @@ -241,13 +253,13 @@ A supported vehicle is one that just works when you install a comma device. All |Toyota|RAV4 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Toyota|RAV4 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Toyota|RAV4 2022|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Toyota|RAV4 2023-24|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|RAV4 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|RAV4 2023-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Toyota|RAV4 Hybrid 2017-18|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Toyota|RAV4 Hybrid 2022|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|Toyota|RAV4 Hybrid 2023-24|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|RAV4 Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|RAV4 Hybrid 2023-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Toyota|Sienna 2018-20|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,13](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 J533 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,13](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 J533 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 9271c15436..a7f61411c8 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -15,6 +15,11 @@ function agnos_init { # set success flag for current boot slot sudo abctl --set_success + # TODO: do this without udev in AGNOS + # udev does this, but sometimes we startup faster + sudo chgrp gpu /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 + sudo chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 + # Check if AGNOS update is required if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then AGNOS_PY="$DIR/system/hardware/tici/agnos.py" diff --git a/launch_env.sh b/launch_env.sh index d86ec63593..6859afb0d4 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1 export VECLIB_MAXIMUM_THREADS=1 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="9.3" + export AGNOS_VERSION="9.7" fi export STAGING_ROOT="/data/safe_staging" diff --git a/opendbc b/opendbc index 7397e466d9..951ab07fdc 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 7397e466d9cfd7f5bc1f49218b8d2afeedec582b +Subproject commit 951ab07fdcbce023a5c927f56bbf94e0f2322366 diff --git a/openpilot/__init__.py b/openpilot/__init__.py index b28b04f643..e69de29bb2 100644 --- a/openpilot/__init__.py +++ b/openpilot/__init__.py @@ -1,3 +0,0 @@ - - - diff --git a/panda b/panda index f48fc21a17..6aa4b55033 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit f48fc21a17079bc04cfb3d8042fd2d67d0aac104 +Subproject commit 6aa4b550336136bc20a6abb307cf310e876eba28 diff --git a/poetry.lock b/poetry.lock index 5b0c6c20b2..9972cd7732 100644 --- a/poetry.lock +++ b/poetry.lock @@ -825,43 +825,43 @@ files = [ [[package]] name = "cryptography" -version = "42.0.2" +version = "42.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"}, - {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"}, - {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4"}, - {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2"}, - {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529"}, - {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1"}, - {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1"}, - {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929"}, - {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9"}, - {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2"}, - {file = "cryptography-42.0.2-cp37-abi3-win32.whl", hash = "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee"}, - {file = "cryptography-42.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee"}, - {file = "cryptography-42.0.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242"}, - {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a"}, - {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446"}, - {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90"}, - {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3"}, - {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589"}, - {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a"}, - {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea"}, - {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33"}, - {file = "cryptography-42.0.2-cp39-abi3-win32.whl", hash = "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635"}, - {file = "cryptography-42.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6"}, - {file = "cryptography-42.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380"}, - {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6"}, - {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2"}, - {file = "cryptography-42.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"}, - {file = "cryptography-42.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008"}, - {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12"}, - {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a"}, - {file = "cryptography-42.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65"}, - {file = "cryptography-42.0.2.tar.gz", hash = "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888"}, + {file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:de5086cd475d67113ccb6f9fae6d8fe3ac54a4f9238fd08bfdb07b03d791ff0a"}, + {file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:935cca25d35dda9e7bd46a24831dfd255307c55a07ff38fd1a92119cffc34857"}, + {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20100c22b298c9eaebe4f0b9032ea97186ac2555f426c3e70670f2517989543b"}, + {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eb6368d5327d6455f20327fb6159b97538820355ec00f8cc9464d617caecead"}, + {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39d5c93e95bcbc4c06313fc6a500cee414ee39b616b55320c1904760ad686938"}, + {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d96ea47ce6d0055d5b97e761d37b4e84195485cb5a38401be341fabf23bc32a"}, + {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d1998e545081da0ab276bcb4b33cce85f775adb86a516e8f55b3dac87f469548"}, + {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93fbee08c48e63d5d1b39ab56fd3fdd02e6c2431c3da0f4edaf54954744c718f"}, + {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:90147dad8c22d64b2ff7331f8d4cddfdc3ee93e4879796f837bdbb2a0b141e0c"}, + {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4dcab7c25e48fc09a73c3e463d09ac902a932a0f8d0c568238b3696d06bf377b"}, + {file = "cryptography-42.0.3-cp37-abi3-win32.whl", hash = "sha256:1e935c2900fb53d31f491c0de04f41110351377be19d83d908c1fd502ae8daa5"}, + {file = "cryptography-42.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:762f3771ae40e111d78d77cbe9c1035e886ac04a234d3ee0856bf4ecb3749d54"}, + {file = "cryptography-42.0.3-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3ec384058b642f7fb7e7bff9664030011ed1af8f852540c76a1317a9dd0d20"}, + {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35772a6cffd1f59b85cb670f12faba05513446f80352fe811689b4e439b5d89e"}, + {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04859aa7f12c2b5f7e22d25198ddd537391f1695df7057c8700f71f26f47a129"}, + {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3d1f5a1d403a8e640fa0887e9f7087331abb3f33b0f2207d2cc7f213e4a864c"}, + {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df34312149b495d9d03492ce97471234fd9037aa5ba217c2a6ea890e9166f151"}, + {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:de4ae486041878dc46e571a4c70ba337ed5233a1344c14a0790c4c4be4bbb8b4"}, + {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0fab2a5c479b360e5e0ea9f654bcebb535e3aa1e493a715b13244f4e07ea8eec"}, + {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25b09b73db78facdfd7dd0fa77a3f19e94896197c86e9f6dc16bce7b37a96504"}, + {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d5cf11bc7f0b71fb71af26af396c83dfd3f6eed56d4b6ef95d57867bf1e4ba65"}, + {file = "cryptography-42.0.3-cp39-abi3-win32.whl", hash = "sha256:0fea01527d4fb22ffe38cd98951c9044400f6eff4788cf52ae116e27d30a1ba3"}, + {file = "cryptography-42.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:2619487f37da18d6826e27854a7f9d4d013c51eafb066c80d09c63cf24505306"}, + {file = "cryptography-42.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ead69ba488f806fe1b1b4050febafdbf206b81fa476126f3e16110c818bac396"}, + {file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:20180da1b508f4aefc101cebc14c57043a02b355d1a652b6e8e537967f1e1b46"}, + {file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fbf0f3f0fac7c089308bd771d2c6c7b7d53ae909dce1db52d8e921f6c19bb3a"}, + {file = "cryptography-42.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c23f03cfd7d9826cdcbad7850de67e18b4654179e01fe9bc623d37c2638eb4ef"}, + {file = "cryptography-42.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db0480ffbfb1193ac4e1e88239f31314fe4c6cdcf9c0b8712b55414afbf80db4"}, + {file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:6c25e1e9c2ce682d01fc5e2dde6598f7313027343bd14f4049b82ad0402e52cd"}, + {file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9541c69c62d7446539f2c1c06d7046aef822940d248fa4b8962ff0302862cc1f"}, + {file = "cryptography-42.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b797099d221df7cce5ff2a1d272761d1554ddf9a987d3e11f6459b38cd300fd"}, + {file = "cryptography-42.0.3.tar.gz", hash = "sha256:069d2ce9be5526a44093a0991c450fe9906cdf069e0e7cd67d9dee49a62b9ebe"}, ] [package.dependencies] @@ -989,22 +989,22 @@ files = [ [[package]] name = "dnspython" -version = "2.5.0" +version = "2.6.1" description = "DNS toolkit" optional = false python-versions = ">=3.8" files = [ - {file = "dnspython-2.5.0-py3-none-any.whl", hash = "sha256:6facdf76b73c742ccf2d07add296f178e629da60be23ce4b0a9c927b1e02c3a6"}, - {file = "dnspython-2.5.0.tar.gz", hash = "sha256:a0034815a59ba9ae888946be7ccca8f7c157b286f8455b379c692efb51022a15"}, + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, ] [package.extras] -dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=5.0.3)", "mypy (>=1.0.1)", "pylint (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0.0)", "sphinx (>=7.0.0)", "twine (>=4.0.0)", "wheel (>=0.41.0)"] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] dnssec = ["cryptography (>=41)"] -doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.25.1)"] -doq = ["aioquic (>=0.9.20)"] -idna = ["idna (>=2.1)"] -trio = ["trio (>=0.14)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=0.9.25)"] +idna = ["idna (>=3.6)"] +trio = ["trio (>=0.23)"] wmi = ["wmi (>=1.5.1)"] [[package]] @@ -1131,60 +1131,60 @@ files = [ [[package]] name = "fonttools" -version = "4.47.2" +version = "4.49.0" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.47.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b629108351d25512d4ea1a8393a2dba325b7b7d7308116b605ea3f8e1be88df"}, - {file = "fonttools-4.47.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c19044256c44fe299d9a73456aabee4b4d06c6b930287be93b533b4737d70aa1"}, - {file = "fonttools-4.47.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8be28c036b9f186e8c7eaf8a11b42373e7e4949f9e9f370202b9da4c4c3f56c"}, - {file = "fonttools-4.47.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f83a4daef6d2a202acb9bf572958f91cfde5b10c8ee7fb1d09a4c81e5d851fd8"}, - {file = "fonttools-4.47.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a5a5318ba5365d992666ac4fe35365f93004109d18858a3e18ae46f67907670"}, - {file = "fonttools-4.47.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8f57ecd742545362a0f7186774b2d1c53423ed9ece67689c93a1055b236f638c"}, - {file = "fonttools-4.47.2-cp310-cp310-win32.whl", hash = "sha256:a1c154bb85dc9a4cf145250c88d112d88eb414bad81d4cb524d06258dea1bdc0"}, - {file = "fonttools-4.47.2-cp310-cp310-win_amd64.whl", hash = "sha256:3e2b95dce2ead58fb12524d0ca7d63a63459dd489e7e5838c3cd53557f8933e1"}, - {file = "fonttools-4.47.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:29495d6d109cdbabe73cfb6f419ce67080c3ef9ea1e08d5750240fd4b0c4763b"}, - {file = "fonttools-4.47.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0a1d313a415eaaba2b35d6cd33536560deeebd2ed758b9bfb89ab5d97dc5deac"}, - {file = "fonttools-4.47.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90f898cdd67f52f18049250a6474185ef6544c91f27a7bee70d87d77a8daf89c"}, - {file = "fonttools-4.47.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3480eeb52770ff75140fe7d9a2ec33fb67b07efea0ab5129c7e0c6a639c40c70"}, - {file = "fonttools-4.47.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0255dbc128fee75fb9be364806b940ed450dd6838672a150d501ee86523ac61e"}, - {file = "fonttools-4.47.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f791446ff297fd5f1e2247c188de53c1bfb9dd7f0549eba55b73a3c2087a2703"}, - {file = "fonttools-4.47.2-cp311-cp311-win32.whl", hash = "sha256:740947906590a878a4bde7dd748e85fefa4d470a268b964748403b3ab2aeed6c"}, - {file = "fonttools-4.47.2-cp311-cp311-win_amd64.whl", hash = "sha256:63fbed184979f09a65aa9c88b395ca539c94287ba3a364517698462e13e457c9"}, - {file = "fonttools-4.47.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4ec558c543609e71b2275c4894e93493f65d2f41c15fe1d089080c1d0bb4d635"}, - {file = "fonttools-4.47.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e040f905d542362e07e72e03612a6270c33d38281fd573160e1003e43718d68d"}, - {file = "fonttools-4.47.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dd58cc03016b281bd2c74c84cdaa6bd3ce54c5a7f47478b7657b930ac3ed8eb"}, - {file = "fonttools-4.47.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32ab2e9702dff0dd4510c7bb958f265a8d3dd5c0e2547e7b5f7a3df4979abb07"}, - {file = "fonttools-4.47.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a808f3c1d1df1f5bf39be869b6e0c263570cdafb5bdb2df66087733f566ea71"}, - {file = "fonttools-4.47.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ac71e2e201df041a2891067dc36256755b1229ae167edbdc419b16da78732c2f"}, - {file = "fonttools-4.47.2-cp312-cp312-win32.whl", hash = "sha256:69731e8bea0578b3c28fdb43dbf95b9386e2d49a399e9a4ad736b8e479b08085"}, - {file = "fonttools-4.47.2-cp312-cp312-win_amd64.whl", hash = "sha256:b3e1304e5f19ca861d86a72218ecce68f391646d85c851742d265787f55457a4"}, - {file = "fonttools-4.47.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:254d9a6f7be00212bf0c3159e0a420eb19c63793b2c05e049eb337f3023c5ecc"}, - {file = "fonttools-4.47.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eabae77a07c41ae0b35184894202305c3ad211a93b2eb53837c2a1143c8bc952"}, - {file = "fonttools-4.47.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86a5ab2873ed2575d0fcdf1828143cfc6b977ac448e3dc616bb1e3d20efbafa"}, - {file = "fonttools-4.47.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13819db8445a0cec8c3ff5f243af6418ab19175072a9a92f6cc8ca7d1452754b"}, - {file = "fonttools-4.47.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4e743935139aa485fe3253fc33fe467eab6ea42583fa681223ea3f1a93dd01e6"}, - {file = "fonttools-4.47.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d49ce3ea7b7173faebc5664872243b40cf88814ca3eb135c4a3cdff66af71946"}, - {file = "fonttools-4.47.2-cp38-cp38-win32.whl", hash = "sha256:94208ea750e3f96e267f394d5588579bb64cc628e321dbb1d4243ffbc291b18b"}, - {file = "fonttools-4.47.2-cp38-cp38-win_amd64.whl", hash = "sha256:0f750037e02beb8b3569fbff701a572e62a685d2a0e840d75816592280e5feae"}, - {file = "fonttools-4.47.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d71606c9321f6701642bd4746f99b6089e53d7e9817fc6b964e90d9c5f0ecc6"}, - {file = "fonttools-4.47.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86e0427864c6c91cf77f16d1fb9bf1bbf7453e824589e8fb8461b6ee1144f506"}, - {file = "fonttools-4.47.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a00bd0e68e88987dcc047ea31c26d40a3c61185153b03457956a87e39d43c37"}, - {file = "fonttools-4.47.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5d77479fb885ef38a16a253a2f4096bc3d14e63a56d6246bfdb56365a12b20c"}, - {file = "fonttools-4.47.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5465df494f20a7d01712b072ae3ee9ad2887004701b95cb2cc6dcb9c2c97a899"}, - {file = "fonttools-4.47.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4c811d3c73b6abac275babb8aa439206288f56fdb2c6f8835e3d7b70de8937a7"}, - {file = "fonttools-4.47.2-cp39-cp39-win32.whl", hash = "sha256:5b60e3afa9635e3dfd3ace2757039593e3bd3cf128be0ddb7a1ff4ac45fa5a50"}, - {file = "fonttools-4.47.2-cp39-cp39-win_amd64.whl", hash = "sha256:7ee48bd9d6b7e8f66866c9090807e3a4a56cf43ffad48962725a190e0dd774c8"}, - {file = "fonttools-4.47.2-py3-none-any.whl", hash = "sha256:7eb7ad665258fba68fd22228a09f347469d95a97fb88198e133595947a20a184"}, - {file = "fonttools-4.47.2.tar.gz", hash = "sha256:7df26dd3650e98ca45f1e29883c96a0b9f5bb6af8d632a6a108bc744fa0bd9b3"}, + {file = "fonttools-4.49.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d970ecca0aac90d399e458f0b7a8a597e08f95de021f17785fb68e2dc0b99717"}, + {file = "fonttools-4.49.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac9a745b7609f489faa65e1dc842168c18530874a5f5b742ac3dd79e26bca8bc"}, + {file = "fonttools-4.49.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ba0e00620ca28d4ca11fc700806fd69144b463aa3275e1b36e56c7c09915559"}, + {file = "fonttools-4.49.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdee3ab220283057e7840d5fb768ad4c2ebe65bdba6f75d5d7bf47f4e0ed7d29"}, + {file = "fonttools-4.49.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ce7033cb61f2bb65d8849658d3786188afd80f53dad8366a7232654804529532"}, + {file = "fonttools-4.49.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:07bc5ea02bb7bc3aa40a1eb0481ce20e8d9b9642a9536cde0218290dd6085828"}, + {file = "fonttools-4.49.0-cp310-cp310-win32.whl", hash = "sha256:86eef6aab7fd7c6c8545f3ebd00fd1d6729ca1f63b0cb4d621bccb7d1d1c852b"}, + {file = "fonttools-4.49.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fac1b7eebfce75ea663e860e7c5b4a8831b858c17acd68263bc156125201abf"}, + {file = "fonttools-4.49.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:edc0cce355984bb3c1d1e89d6a661934d39586bb32191ebff98c600f8957c63e"}, + {file = "fonttools-4.49.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:83a0d9336de2cba86d886507dd6e0153df333ac787377325a39a2797ec529814"}, + {file = "fonttools-4.49.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36c8865bdb5cfeec88f5028e7e592370a0657b676c6f1d84a2108e0564f90e22"}, + {file = "fonttools-4.49.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33037d9e56e2562c710c8954d0f20d25b8386b397250d65581e544edc9d6b942"}, + {file = "fonttools-4.49.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8fb022d799b96df3eaa27263e9eea306bd3d437cc9aa981820850281a02b6c9a"}, + {file = "fonttools-4.49.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33c584c0ef7dc54f5dd4f84082eabd8d09d1871a3d8ca2986b0c0c98165f8e86"}, + {file = "fonttools-4.49.0-cp311-cp311-win32.whl", hash = "sha256:cbe61b158deb09cffdd8540dc4a948d6e8f4d5b4f3bf5cd7db09bd6a61fee64e"}, + {file = "fonttools-4.49.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc11e5114f3f978d0cea7e9853627935b30d451742eeb4239a81a677bdee6bf6"}, + {file = "fonttools-4.49.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d647a0e697e5daa98c87993726da8281c7233d9d4ffe410812a4896c7c57c075"}, + {file = "fonttools-4.49.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f3bbe672df03563d1f3a691ae531f2e31f84061724c319652039e5a70927167e"}, + {file = "fonttools-4.49.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bebd91041dda0d511b0d303180ed36e31f4f54b106b1259b69fade68413aa7ff"}, + {file = "fonttools-4.49.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4145f91531fd43c50f9eb893faa08399816bb0b13c425667c48475c9f3a2b9b5"}, + {file = "fonttools-4.49.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea329dafb9670ffbdf4dbc3b0e5c264104abcd8441d56de77f06967f032943cb"}, + {file = "fonttools-4.49.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c076a9e548521ecc13d944b1d261ff3d7825048c338722a4bd126d22316087b7"}, + {file = "fonttools-4.49.0-cp312-cp312-win32.whl", hash = "sha256:b607ea1e96768d13be26d2b400d10d3ebd1456343eb5eaddd2f47d1c4bd00880"}, + {file = "fonttools-4.49.0-cp312-cp312-win_amd64.whl", hash = "sha256:a974c49a981e187381b9cc2c07c6b902d0079b88ff01aed34695ec5360767034"}, + {file = "fonttools-4.49.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b85ec0bdd7bdaa5c1946398cbb541e90a6dfc51df76dfa88e0aaa41b335940cb"}, + {file = "fonttools-4.49.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:af20acbe198a8a790618ee42db192eb128afcdcc4e96d99993aca0b60d1faeb4"}, + {file = "fonttools-4.49.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d418b1fee41a1d14931f7ab4b92dc0bc323b490e41d7a333eec82c9f1780c75"}, + {file = "fonttools-4.49.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b44a52b8e6244b6548851b03b2b377a9702b88ddc21dcaf56a15a0393d425cb9"}, + {file = "fonttools-4.49.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7c7125068e04a70739dad11857a4d47626f2b0bd54de39e8622e89701836eabd"}, + {file = "fonttools-4.49.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29e89d0e1a7f18bc30f197cfadcbef5a13d99806447c7e245f5667579a808036"}, + {file = "fonttools-4.49.0-cp38-cp38-win32.whl", hash = "sha256:9d95fa0d22bf4f12d2fb7b07a46070cdfc19ef5a7b1c98bc172bfab5bf0d6844"}, + {file = "fonttools-4.49.0-cp38-cp38-win_amd64.whl", hash = "sha256:768947008b4dc552d02772e5ebd49e71430a466e2373008ce905f953afea755a"}, + {file = "fonttools-4.49.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:08877e355d3dde1c11973bb58d4acad1981e6d1140711230a4bfb40b2b937ccc"}, + {file = "fonttools-4.49.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fdb54b076f25d6b0f0298dc706acee5052de20c83530fa165b60d1f2e9cbe3cb"}, + {file = "fonttools-4.49.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0af65c720520710cc01c293f9c70bd69684365c6015cc3671db2b7d807fe51f2"}, + {file = "fonttools-4.49.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f255ce8ed7556658f6d23f6afd22a6d9bbc3edb9b96c96682124dc487e1bf42"}, + {file = "fonttools-4.49.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d00af0884c0e65f60dfaf9340e26658836b935052fdd0439952ae42e44fdd2be"}, + {file = "fonttools-4.49.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:263832fae27481d48dfafcc43174644b6706639661e242902ceb30553557e16c"}, + {file = "fonttools-4.49.0-cp39-cp39-win32.whl", hash = "sha256:0404faea044577a01bb82d47a8fa4bc7a54067fa7e324785dd65d200d6dd1133"}, + {file = "fonttools-4.49.0-cp39-cp39-win_amd64.whl", hash = "sha256:b050d362df50fc6e38ae3954d8c29bf2da52be384649ee8245fdb5186b620836"}, + {file = "fonttools-4.49.0-py3-none-any.whl", hash = "sha256:af281525e5dd7fa0b39fb1667b8d5ca0e2a9079967e14c4bfe90fd1cd13e0f18"}, + {file = "fonttools-4.49.0.tar.gz", hash = "sha256:ebf46e7f01b7af7861310417d7c49591a85d99146fc23a5ba82fdb28af156321"}, ] [package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] graphite = ["lz4 (>=1.7.4.2)"] interpolatable = ["munkres", "pycairo", "scipy"] -lxml = ["lxml (>=4.0,<5)"] +lxml = ["lxml (>=4.0)"] pathops = ["skia-pathops (>=0.5.0)"] plot = ["matplotlib"] repacker = ["uharfbuzz (>=0.23.0)"] @@ -1563,13 +1563,13 @@ zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2022.1)"] [[package]] name = "identify" -version = "2.5.33" +version = "2.5.35" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, - {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, + {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, + {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, ] [package.extras] @@ -2116,39 +2116,39 @@ files = [ [[package]] name = "matplotlib" -version = "3.8.2" +version = "3.8.3" description = "Python plotting package" optional = false python-versions = ">=3.9" files = [ - {file = "matplotlib-3.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:09796f89fb71a0c0e1e2f4bdaf63fb2cefc84446bb963ecdeb40dfee7dfa98c7"}, - {file = "matplotlib-3.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9c6976748a25e8b9be51ea028df49b8e561eed7809146da7a47dbecebab367"}, - {file = "matplotlib-3.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78e4f2cedf303869b782071b55fdde5987fda3038e9d09e58c91cc261b5ad18"}, - {file = "matplotlib-3.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e208f46cf6576a7624195aa047cb344a7f802e113bb1a06cfd4bee431de5e31"}, - {file = "matplotlib-3.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46a569130ff53798ea5f50afce7406e91fdc471ca1e0e26ba976a8c734c9427a"}, - {file = "matplotlib-3.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:830f00640c965c5b7f6bc32f0d4ce0c36dfe0379f7dd65b07a00c801713ec40a"}, - {file = "matplotlib-3.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d86593ccf546223eb75a39b44c32788e6f6440d13cfc4750c1c15d0fcb850b63"}, - {file = "matplotlib-3.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a5430836811b7652991939012f43d2808a2db9b64ee240387e8c43e2e5578c8"}, - {file = "matplotlib-3.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9576723858a78751d5aacd2497b8aef29ffea6d1c95981505877f7ac28215c6"}, - {file = "matplotlib-3.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ba9cbd8ac6cf422f3102622b20f8552d601bf8837e49a3afed188d560152788"}, - {file = "matplotlib-3.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:03f9d160a29e0b65c0790bb07f4f45d6a181b1ac33eb1bb0dd225986450148f0"}, - {file = "matplotlib-3.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:3773002da767f0a9323ba1a9b9b5d00d6257dbd2a93107233167cfb581f64717"}, - {file = "matplotlib-3.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4c318c1e95e2f5926fba326f68177dee364aa791d6df022ceb91b8221bd0a627"}, - {file = "matplotlib-3.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:091275d18d942cf1ee9609c830a1bc36610607d8223b1b981c37d5c9fc3e46a4"}, - {file = "matplotlib-3.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b0f3b8ea0e99e233a4bcc44590f01604840d833c280ebb8fe5554fd3e6cfe8d"}, - {file = "matplotlib-3.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7b1704a530395aaf73912be741c04d181f82ca78084fbd80bc737be04848331"}, - {file = "matplotlib-3.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533b0e3b0c6768eef8cbe4b583731ce25a91ab54a22f830db2b031e83cca9213"}, - {file = "matplotlib-3.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:0f4fc5d72b75e2c18e55eb32292659cf731d9d5b312a6eb036506304f4675630"}, - {file = "matplotlib-3.8.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:deaed9ad4da0b1aea77fe0aa0cebb9ef611c70b3177be936a95e5d01fa05094f"}, - {file = "matplotlib-3.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:172f4d0fbac3383d39164c6caafd3255ce6fa58f08fc392513a0b1d3b89c4f89"}, - {file = "matplotlib-3.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7d36c2209d9136cd8e02fab1c0ddc185ce79bc914c45054a9f514e44c787917"}, - {file = "matplotlib-3.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5864bdd7da445e4e5e011b199bb67168cdad10b501750367c496420f2ad00843"}, - {file = "matplotlib-3.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ef8345b48e95cee45ff25192ed1f4857273117917a4dcd48e3905619bcd9c9b8"}, - {file = "matplotlib-3.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:7c48d9e221b637c017232e3760ed30b4e8d5dfd081daf327e829bf2a72c731b4"}, - {file = "matplotlib-3.8.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa11b3c6928a1e496c1a79917d51d4cd5d04f8a2e75f21df4949eeefdf697f4b"}, - {file = "matplotlib-3.8.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1095fecf99eeb7384dabad4bf44b965f929a5f6079654b681193edf7169ec20"}, - {file = "matplotlib-3.8.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:bddfb1db89bfaa855912261c805bd0e10218923cc262b9159a49c29a7a1c1afa"}, - {file = "matplotlib-3.8.2.tar.gz", hash = "sha256:01a978b871b881ee76017152f1f1a0cbf6bd5f7b8ff8c96df0df1bd57d8755a1"}, + {file = "matplotlib-3.8.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cf60138ccc8004f117ab2a2bad513cc4d122e55864b4fe7adf4db20ca68a078f"}, + {file = "matplotlib-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f557156f7116be3340cdeef7f128fa99b0d5d287d5f41a16e169819dcf22357"}, + {file = "matplotlib-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f386cf162b059809ecfac3bcc491a9ea17da69fa35c8ded8ad154cd4b933d5ec"}, + {file = "matplotlib-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c5f96f57b0369c288bf6f9b5274ba45787f7e0589a34d24bdbaf6d3344632f"}, + {file = "matplotlib-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:83e0f72e2c116ca7e571c57aa29b0fe697d4c6425c4e87c6e994159e0c008635"}, + {file = "matplotlib-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:1c5c8290074ba31a41db1dc332dc2b62def469ff33766cbe325d32a3ee291aea"}, + {file = "matplotlib-3.8.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5184e07c7e1d6d1481862ee361905b7059f7fe065fc837f7c3dc11eeb3f2f900"}, + {file = "matplotlib-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7e7e0993d0758933b1a241a432b42c2db22dfa37d4108342ab4afb9557cbe3e"}, + {file = "matplotlib-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04b36ad07eac9740fc76c2aa16edf94e50b297d6eb4c081e3add863de4bb19a7"}, + {file = "matplotlib-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c42dae72a62f14982f1474f7e5c9959fc4bc70c9de11cc5244c6e766200ba65"}, + {file = "matplotlib-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf5932eee0d428192c40b7eac1399d608f5d995f975cdb9d1e6b48539a5ad8d0"}, + {file = "matplotlib-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:40321634e3a05ed02abf7c7b47a50be50b53ef3eaa3a573847431a545585b407"}, + {file = "matplotlib-3.8.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:09074f8057917d17ab52c242fdf4916f30e99959c1908958b1fc6032e2d0f6d4"}, + {file = "matplotlib-3.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5745f6d0fb5acfabbb2790318db03809a253096e98c91b9a31969df28ee604aa"}, + {file = "matplotlib-3.8.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97653d869a71721b639714b42d87cda4cfee0ee74b47c569e4874c7590c55c5"}, + {file = "matplotlib-3.8.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:242489efdb75b690c9c2e70bb5c6550727058c8a614e4c7716f363c27e10bba1"}, + {file = "matplotlib-3.8.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:83c0653c64b73926730bd9ea14aa0f50f202ba187c307a881673bad4985967b7"}, + {file = "matplotlib-3.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef6c1025a570354297d6c15f7d0f296d95f88bd3850066b7f1e7b4f2f4c13a39"}, + {file = "matplotlib-3.8.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c4af3f7317f8a1009bbb2d0bf23dfaba859eb7dd4ccbd604eba146dccaaaf0a4"}, + {file = "matplotlib-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c6e00a65d017d26009bac6808f637b75ceade3e1ff91a138576f6b3065eeeba"}, + {file = "matplotlib-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7b49ab49a3bea17802df6872f8d44f664ba8f9be0632a60c99b20b6db2165b7"}, + {file = "matplotlib-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6728dde0a3997396b053602dbd907a9bd64ec7d5cf99e728b404083698d3ca01"}, + {file = "matplotlib-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:813925d08fb86aba139f2d31864928d67511f64e5945ca909ad5bc09a96189bb"}, + {file = "matplotlib-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:cd3a0c2be76f4e7be03d34a14d49ded6acf22ef61f88da600a18a5cd8b3c5f3c"}, + {file = "matplotlib-3.8.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fa93695d5c08544f4a0dfd0965f378e7afc410d8672816aff1e81be1f45dbf2e"}, + {file = "matplotlib-3.8.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9764df0e8778f06414b9d281a75235c1e85071f64bb5d71564b97c1306a2afc"}, + {file = "matplotlib-3.8.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5e431a09e6fab4012b01fc155db0ce6dccacdbabe8198197f523a4ef4805eb26"}, + {file = "matplotlib-3.8.3.tar.gz", hash = "sha256:7b416239e9ae38be54b028abbf9048aff5054a9aba5416bef0bd17f9162ce161"}, ] [package.dependencies] @@ -2194,13 +2194,13 @@ files = [ [[package]] name = "metadrive-simulator" -version = "0.4.2.2" +version = "0.4.2.3" description = "An open-ended driving simulator with infinite scenes" optional = false python-versions = ">=3.6, <3.12" files = [ - {file = "metadrive-simulator-0.4.2.2.tar.gz", hash = "sha256:dcce9f9c73b6055e70480af8543058b95e8c4f68d2595107f3ef36c9be03f6bb"}, - {file = "metadrive_simulator-0.4.2.2-py3-none-any.whl", hash = "sha256:165ba0b5275313a71090ba0e73d51b7b5e05391b071d3a2114d999ac9287d150"}, + {file = "metadrive-simulator-0.4.2.3.tar.gz", hash = "sha256:bcda7d07146128161b0bc2cc337e01612b9222202706370043b52f7936a8a277"}, + {file = "metadrive_simulator-0.4.2.3-py3-none-any.whl", hash = "sha256:f6fff20b931bb956c55e0e81bf1f6b641169a84a4c853435a8b26bd51c8e9876"}, ] [package.dependencies] @@ -3041,17 +3041,17 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "polyline" -version = "2.0.1" +version = "2.0.2" description = "A Python implementation of Google's Encoded Polyline Algorithm Format." optional = false python-versions = ">=3.7" files = [ - {file = "polyline-2.0.1-py3-none-any.whl", hash = "sha256:7b1ff647be393143c1b9268738d9efb98a327dd0b29f9c3b84552dff0e34ea3c"}, - {file = "polyline-2.0.1.tar.gz", hash = "sha256:74cb5cea098dddf09d1a5a1f17af9184d371cbf3e9723de0194e530ec39ca1f6"}, + {file = "polyline-2.0.2-py3-none-any.whl", hash = "sha256:389655c893bdabf2863c6aaa49490cf83dcdcec86ae715f67044ee98be57bef5"}, + {file = "polyline-2.0.2.tar.gz", hash = "sha256:10541e759c5fd51f746ee304e9af94744089a4055b6257b293b3afd1df64e369"}, ] [package.extras] -dev = ["pylint (>=2.15.10,<2.16.0)", "pytest (>=7.0,<8.0)", "pytest-cov (>=4.0,<5.0)", "sphinx (>=4.2.0,<4.3.0)", "sphinx-rtd-theme (>=1.0.0,<1.1.0)", "toml (>=0.10.2,<0.11.0)"] +dev = ["pylint (>=3.0.3,<3.1.0)", "pytest (>=7.0,<8.0)", "pytest-cov (>=4.0,<5.0)", "sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.0,<1.3.0)", "toml (>=0.10.2,<0.11.0)"] publish = ["build (>=0.8,<1.0)", "twine (>=4.0,<5.0)"] [[package]] @@ -3085,13 +3085,13 @@ files = [ [[package]] name = "pre-commit" -version = "3.6.0" +version = "3.6.2" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, - {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, + {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, + {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, ] [package.dependencies] @@ -3113,22 +3113,22 @@ files = [ [[package]] name = "protobuf" -version = "4.25.2" +version = "4.25.3" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6"}, - {file = "protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9"}, - {file = "protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d"}, - {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62"}, - {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020"}, - {file = "protobuf-4.25.2-cp38-cp38-win32.whl", hash = "sha256:33a1aeef4b1927431d1be780e87b641e322b88d654203a9e9d93f218ee359e61"}, - {file = "protobuf-4.25.2-cp38-cp38-win_amd64.whl", hash = "sha256:47f3de503fe7c1245f6f03bea7e8d3ec11c6c4a2ea9ef910e3221c8a15516d62"}, - {file = "protobuf-4.25.2-cp39-cp39-win32.whl", hash = "sha256:5e5c933b4c30a988b52e0b7c02641760a5ba046edc5e43d3b94a74c9fc57c1b3"}, - {file = "protobuf-4.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:d66a769b8d687df9024f2985d5137a337f957a0916cf5464d1513eee96a63ff0"}, - {file = "protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830"}, - {file = "protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e"}, + {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, + {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, + {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, + {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, + {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, + {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, + {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, + {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, + {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, ] [[package]] @@ -6468,13 +6468,13 @@ cp2110 = ["hidapi"] [[package]] name = "pytest" -version = "8.0.0" +version = "8.0.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, - {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, + {file = "pytest-8.0.1-py3-none-any.whl", hash = "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca"}, + {file = "pytest-8.0.1.tar.gz", hash = "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae"}, ] [package.dependencies] @@ -6639,12 +6639,12 @@ numpy = ["numpy (>=1.6.0)"] [[package]] name = "pytweening" -version = "1.0.7" +version = "1.1.0" description = "A collection of tweening / easing functions." optional = false python-versions = "*" files = [ - {file = "pytweening-1.0.7.tar.gz", hash = "sha256:767134f1bf57b76c1ce9f692dd1cfc776d9a279de6724e8d04854508fd7ededb"}, + {file = "pytweening-1.1.0.tar.gz", hash = "sha256:0d8e14af529dd816ad4aa4a86757dfb5fe2fc2897e06f5db60183706a9370828"}, ] [[package]] @@ -6924,28 +6924,28 @@ docs = ["furo (==2023.9.10)", "pyenchant (==3.2.2)", "sphinx (==7.1.2)", "sphinx [[package]] name = "ruff" -version = "0.2.0" +version = "0.2.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.2.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:638ea3294f800d18bae84a492cb5a245c8d29c90d19a91d8e338937a4c27fca0"}, - {file = "ruff-0.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff35433fcf4dff6d610738712152df6b7d92351a1bde8e00bd405b08b3d5759"}, - {file = "ruff-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf9faafbdcf4f53917019f2c230766da437d4fd5caecd12ddb68bb6a17d74399"}, - {file = "ruff-0.2.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8153a3e4128ed770871c47545f1ae7b055023e0c222ff72a759f5a341ee06483"}, - {file = "ruff-0.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a75a98ae989a27090e9c51f763990ad5bbc92d20626d54e9701c7fe597f399"}, - {file = "ruff-0.2.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:87057dd2fdde297130ff99553be8549ca38a2965871462a97394c22ed2dfc19d"}, - {file = "ruff-0.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d232f99d3ab00094ebaf88e0fb7a8ccacaa54cc7fa3b8993d9627a11e6aed7a"}, - {file = "ruff-0.2.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d3c641f95f435fc6754b05591774a17df41648f0daf3de0d75ad3d9f099ab92"}, - {file = "ruff-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3826fb34c144ef1e171b323ed6ae9146ab76d109960addca730756dc19dc7b22"}, - {file = "ruff-0.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eceab7d85d09321b4de18b62d38710cf296cb49e98979960a59c6b9307c18cfe"}, - {file = "ruff-0.2.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:30ad74687e1f4a9ff8e513b20b82ccadb6bd796fe5697f1e417189c5cde6be3e"}, - {file = "ruff-0.2.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a7e3818698f8460bd0f8d4322bbe99db8327e9bc2c93c789d3159f5b335f47da"}, - {file = "ruff-0.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:edf23041242c48b0d8295214783ef543847ef29e8226d9f69bf96592dba82a83"}, - {file = "ruff-0.2.0-py3-none-win32.whl", hash = "sha256:e155147199c2714ff52385b760fe242bb99ea64b240a9ffbd6a5918eb1268843"}, - {file = "ruff-0.2.0-py3-none-win_amd64.whl", hash = "sha256:ba918e01cdd21e81b07555564f40d307b0caafa9a7a65742e98ff244f5035c59"}, - {file = "ruff-0.2.0-py3-none-win_arm64.whl", hash = "sha256:3fbaff1ba9564a2c5943f8f38bc221f04bac687cc7485e45237579fee7ccda79"}, - {file = "ruff-0.2.0.tar.gz", hash = "sha256:63856b91837606c673537d2889989733d7dffde553828d3b0f0bacfa6def54be"}, + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, + {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, + {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, + {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, + {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, ] [[package]] @@ -7027,13 +7027,13 @@ stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"] [[package]] name = "sentry-sdk" -version = "1.40.0" +version = "1.40.5" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = "*" files = [ - {file = "sentry-sdk-1.40.0.tar.gz", hash = "sha256:34ad8cfc9b877aaa2a8eb86bfe5296a467fffe0619b931a05b181c45f6da59bf"}, - {file = "sentry_sdk-1.40.0-py2.py3-none-any.whl", hash = "sha256:78575620331186d32f34b7ece6edea97ce751f58df822547d3ab85517881a27a"}, + {file = "sentry-sdk-1.40.5.tar.gz", hash = "sha256:d2dca2392cc5c9a2cc9bb874dd7978ebb759682fe4fe889ee7e970ee8dd1c61e"}, + {file = "sentry_sdk-1.40.5-py2.py3-none-any.whl", hash = "sha256:d188b407c9bacbe2a50a824e1f8fb99ee1aeb309133310488c570cb6d7056643"}, ] [package.dependencies] @@ -7172,72 +7172,72 @@ test = ["pytest"] [[package]] name = "setuptools" -version = "69.0.3" +version = "69.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, - {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, + {file = "setuptools-69.1.0-py3-none-any.whl", hash = "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6"}, + {file = "setuptools-69.1.0.tar.gz", hash = "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "shapely" -version = "2.0.2" +version = "2.0.3" description = "Manipulation and analysis of geometric objects" optional = false python-versions = ">=3.7" files = [ - {file = "shapely-2.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6ca8cffbe84ddde8f52b297b53f8e0687bd31141abb2c373fd8a9f032df415d6"}, - {file = "shapely-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:baa14fc27771e180c06b499a0a7ba697c7988c7b2b6cba9a929a19a4d2762de3"}, - {file = "shapely-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36480e32c434d168cdf2f5e9862c84aaf4d714a43a8465ae3ce8ff327f0affb7"}, - {file = "shapely-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef753200cbffd4f652efb2c528c5474e5a14341a473994d90ad0606522a46a2"}, - {file = "shapely-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9a41ff4323fc9d6257759c26eb1cf3a61ebc7e611e024e6091f42977303fd3a"}, - {file = "shapely-2.0.2-cp310-cp310-win32.whl", hash = "sha256:72b5997272ae8c25f0fd5b3b967b3237e87fab7978b8d6cd5fa748770f0c5d68"}, - {file = "shapely-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:34eac2337cbd67650248761b140d2535855d21b969d76d76123317882d3a0c1a"}, - {file = "shapely-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b0c052709c8a257c93b0d4943b0b7a3035f87e2d6a8ac9407b6a992d206422f"}, - {file = "shapely-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d217e56ae067e87b4e1731d0dc62eebe887ced729ba5c2d4590e9e3e9fdbd88"}, - {file = "shapely-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94ac128ae2ab4edd0bffcd4e566411ea7bdc738aeaf92c32a8a836abad725f9f"}, - {file = "shapely-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3ee28f5e63a130ec5af4dc3c4cb9c21c5788bb13c15e89190d163b14f9fb89"}, - {file = "shapely-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:737dba15011e5a9b54a8302f1748b62daa207c9bc06f820cd0ad32a041f1c6f2"}, - {file = "shapely-2.0.2-cp311-cp311-win32.whl", hash = "sha256:45ac6906cff0765455a7b49c1670af6e230c419507c13e2f75db638c8fc6f3bd"}, - {file = "shapely-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:dc9342fc82e374130db86a955c3c4525bfbf315a248af8277a913f30911bed9e"}, - {file = "shapely-2.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:06f193091a7c6112fc08dfd195a1e3846a64306f890b151fa8c63b3e3624202c"}, - {file = "shapely-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eebe544df5c018134f3c23b6515877f7e4cd72851f88a8d0c18464f414d141a2"}, - {file = "shapely-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7e92e7c255f89f5cdf777690313311f422aa8ada9a3205b187113274e0135cd8"}, - {file = "shapely-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be46d5509b9251dd9087768eaf35a71360de6afac82ce87c636990a0871aa18b"}, - {file = "shapely-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5533a925d8e211d07636ffc2fdd9a7f9f13d54686d00577eeb11d16f00be9c4"}, - {file = "shapely-2.0.2-cp312-cp312-win32.whl", hash = "sha256:084b023dae8ad3d5b98acee9d3bf098fdf688eb0bb9b1401e8b075f6a627b611"}, - {file = "shapely-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:ea84d1cdbcf31e619d672b53c4532f06253894185ee7acb8ceb78f5f33cbe033"}, - {file = "shapely-2.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ed1e99702125e7baccf401830a3b94d810d5c70b329b765fe93451fe14cf565b"}, - {file = "shapely-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7d897e6bdc6bc64f7f65155dbbb30e49acaabbd0d9266b9b4041f87d6e52b3a"}, - {file = "shapely-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0521d76d1e8af01e712db71da9096b484f081e539d4f4a8c97342e7971d5e1b4"}, - {file = "shapely-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:5324be299d4c533ecfcfd43424dfd12f9428fd6f12cda38a4316da001d6ef0ea"}, - {file = "shapely-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:78128357a0cee573257a0c2c388d4b7bf13cb7dbe5b3fe5d26d45ebbe2a39e25"}, - {file = "shapely-2.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87dc2be34ac3a3a4a319b963c507ac06682978a5e6c93d71917618b14f13066e"}, - {file = "shapely-2.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:42997ac806e4583dad51c80a32d38570fd9a3d4778f5e2c98f9090aa7db0fe91"}, - {file = "shapely-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ccfd5fa10a37e67dbafc601c1ddbcbbfef70d34c3f6b0efc866ddbdb55893a6c"}, - {file = "shapely-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7c95d3379ae3abb74058938a9fcbc478c6b2e28d20dace38f8b5c587dde90aa"}, - {file = "shapely-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a21353d28209fb0d8cc083e08ca53c52666e0d8a1f9bbe23b6063967d89ed24"}, - {file = "shapely-2.0.2-cp38-cp38-win32.whl", hash = "sha256:03e63a99dfe6bd3beb8d5f41ec2086585bb969991d603f9aeac335ad396a06d4"}, - {file = "shapely-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:c6fd29fbd9cd76350bd5cc14c49de394a31770aed02d74203e23b928f3d2f1aa"}, - {file = "shapely-2.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f217d28ecb48e593beae20a0082a95bd9898d82d14b8fcb497edf6bff9a44d7"}, - {file = "shapely-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:394e5085b49334fd5b94fa89c086edfb39c3ecab7f669e8b2a4298b9d523b3a5"}, - {file = "shapely-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd3ad17b64466a033848c26cb5b509625c87d07dcf39a1541461cacdb8f7e91c"}, - {file = "shapely-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d41a116fcad58048d7143ddb01285e1a8780df6dc1f56c3b1e1b7f12ed296651"}, - {file = "shapely-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dea9a0651333cf96ef5bb2035044e3ad6a54f87d90e50fe4c2636debf1b77abc"}, - {file = "shapely-2.0.2-cp39-cp39-win32.whl", hash = "sha256:b8eb0a92f7b8c74f9d8fdd1b40d395113f59bd8132ca1348ebcc1f5aece94b96"}, - {file = "shapely-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:794affd80ca0f2c536fc948a3afa90bd8fb61ebe37fe873483ae818e7f21def4"}, - {file = "shapely-2.0.2.tar.gz", hash = "sha256:1713cc04c171baffc5b259ba8531c58acc2a301707b7f021d88a15ed090649e7"}, -] - -[package.dependencies] -numpy = ">=1.14" + {file = "shapely-2.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:af7e9abe180b189431b0f490638281b43b84a33a960620e6b2e8d3e3458b61a1"}, + {file = "shapely-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98040462b36ced9671e266b95c326b97f41290d9d17504a1ee4dc313a7667b9c"}, + {file = "shapely-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:71eb736ef2843f23473c6e37f6180f90f0a35d740ab284321548edf4e55d9a52"}, + {file = "shapely-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:881eb9dbbb4a6419667e91fcb20313bfc1e67f53dbb392c6840ff04793571ed1"}, + {file = "shapely-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f10d2ccf0554fc0e39fad5886c839e47e207f99fdf09547bc687a2330efda35b"}, + {file = "shapely-2.0.3-cp310-cp310-win32.whl", hash = "sha256:6dfdc077a6fcaf74d3eab23a1ace5abc50c8bce56ac7747d25eab582c5a2990e"}, + {file = "shapely-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:64c5013dacd2d81b3bb12672098a0b2795c1bf8190cfc2980e380f5ef9d9e4d9"}, + {file = "shapely-2.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:56cee3e4e8159d6f2ce32e421445b8e23154fd02a0ac271d6a6c0b266a8e3cce"}, + {file = "shapely-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:619232c8276fded09527d2a9fd91a7885ff95c0ff9ecd5e3cb1e34fbb676e2ae"}, + {file = "shapely-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2a7d256db6f5b4b407dc0c98dd1b2fcf1c9c5814af9416e5498d0a2e4307a4b"}, + {file = "shapely-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45f0c8cd4583647db3216d965d49363e6548c300c23fd7e57ce17a03f824034"}, + {file = "shapely-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13cb37d3826972a82748a450328fe02a931dcaed10e69a4d83cc20ba021bc85f"}, + {file = "shapely-2.0.3-cp311-cp311-win32.whl", hash = "sha256:9302d7011e3e376d25acd30d2d9e70d315d93f03cc748784af19b00988fc30b1"}, + {file = "shapely-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6b464f2666b13902835f201f50e835f2f153f37741db88f68c7f3b932d3505fa"}, + {file = "shapely-2.0.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e86e7cb8e331a4850e0c2a8b2d66dc08d7a7b301b8d1d34a13060e3a5b4b3b55"}, + {file = "shapely-2.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c91981c99ade980fc49e41a544629751a0ccd769f39794ae913e53b07b2f78b9"}, + {file = "shapely-2.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd45d456983dc60a42c4db437496d3f08a4201fbf662b69779f535eb969660af"}, + {file = "shapely-2.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:882fb1ffc7577e88c1194f4f1757e277dc484ba096a3b94844319873d14b0f2d"}, + {file = "shapely-2.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9f2d93bff2ea52fa93245798cddb479766a18510ea9b93a4fb9755c79474889"}, + {file = "shapely-2.0.3-cp312-cp312-win32.whl", hash = "sha256:99abad1fd1303b35d991703432c9481e3242b7b3a393c186cfb02373bf604004"}, + {file = "shapely-2.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:6f555fe3304a1f40398977789bc4fe3c28a11173196df9ece1e15c5bc75a48db"}, + {file = "shapely-2.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983cc418c1fa160b7d797cfef0e0c9f8c6d5871e83eae2c5793fce6a837fad9"}, + {file = "shapely-2.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18bddb8c327f392189a8d5d6b9a858945722d0bb95ccbd6a077b8e8fc4c7890d"}, + {file = "shapely-2.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:442f4dcf1eb58c5a4e3428d88e988ae153f97ab69a9f24e07bf4af8038536325"}, + {file = "shapely-2.0.3-cp37-cp37m-win32.whl", hash = "sha256:31a40b6e3ab00a4fd3a1d44efb2482278642572b8e0451abdc8e0634b787173e"}, + {file = "shapely-2.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:59b16976c2473fec85ce65cc9239bef97d4205ab3acead4e6cdcc72aee535679"}, + {file = "shapely-2.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:705efbce1950a31a55b1daa9c6ae1c34f1296de71ca8427974ec2f27d57554e3"}, + {file = "shapely-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:601c5c0058a6192df704cb889439f64994708563f57f99574798721e9777a44b"}, + {file = "shapely-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f24ecbb90a45c962b3b60d8d9a387272ed50dc010bfe605f1d16dfc94772d8a1"}, + {file = "shapely-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8c2a2989222c6062f7a0656e16276c01bb308bc7e5d999e54bf4e294ce62e76"}, + {file = "shapely-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42bceb9bceb3710a774ce04908fda0f28b291323da2688f928b3f213373b5aee"}, + {file = "shapely-2.0.3-cp38-cp38-win32.whl", hash = "sha256:54d925c9a311e4d109ec25f6a54a8bd92cc03481a34ae1a6a92c1fe6729b7e01"}, + {file = "shapely-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:300d203b480a4589adefff4c4af0b13919cd6d760ba3cbb1e56275210f96f654"}, + {file = "shapely-2.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:083d026e97b6c1f4a9bd2a9171c7692461092ed5375218170d91705550eecfd5"}, + {file = "shapely-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:27b6e1910094d93e9627f2664121e0e35613262fc037051680a08270f6058daf"}, + {file = "shapely-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:71b2de56a9e8c0e5920ae5ddb23b923490557ac50cb0b7fa752761bf4851acde"}, + {file = "shapely-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d279e56bbb68d218d63f3efc80c819cedcceef0e64efbf058a1df89dc57201b"}, + {file = "shapely-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88566d01a30f0453f7d038db46bc83ce125e38e47c5f6bfd4c9c287010e9bf74"}, + {file = "shapely-2.0.3-cp39-cp39-win32.whl", hash = "sha256:58afbba12c42c6ed44c4270bc0e22f3dadff5656d711b0ad335c315e02d04707"}, + {file = "shapely-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:5026b30433a70911979d390009261b8c4021ff87c7c3cbd825e62bb2ffa181bc"}, + {file = "shapely-2.0.3.tar.gz", hash = "sha256:4d65d0aa7910af71efa72fd6447e02a8e5dd44da81a983de9d736d6e6ccbe674"}, +] + +[package.dependencies] +numpy = ">=1.14,<2" [package.extras] docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"] @@ -7574,13 +7574,13 @@ files = [ [[package]] name = "tqdm" -version = "4.66.1" +version = "4.66.2" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, - {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, + {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"}, + {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"}, ] [package.dependencies] @@ -7594,13 +7594,13 @@ telegram = ["requests"] [[package]] name = "types-requests" -version = "2.31.0.20240125" +version = "2.31.0.20240218" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" files = [ - {file = "types-requests-2.31.0.20240125.tar.gz", hash = "sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5"}, - {file = "types_requests-2.31.0.20240125-py3-none-any.whl", hash = "sha256:9592a9a4cb92d6d75d9b491a41477272b710e021011a2a3061157e2fb1f1a5d1"}, + {file = "types-requests-2.31.0.20240218.tar.gz", hash = "sha256:f1721dba8385958f504a5386240b92de4734e047a08a40751c1654d1ac3349c5"}, + {file = "types_requests-2.31.0.20240218-py3-none-any.whl", hash = "sha256:a82807ec6ddce8f00fe0e949da6d6bc1fbf1715420218a9640d695f70a9e5a9b"}, ] [package.dependencies] @@ -7630,24 +7630,24 @@ files = [ [[package]] name = "tzdata" -version = "2023.4" +version = "2024.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, - {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] [[package]] name = "urllib3" -version = "2.2.0" +version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, - {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] @@ -7829,4 +7829,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "~3.11" -content-hash = "8af315a175ab43dbc5a777f68fca6d76af1443b5b574ab8570ef5dad59f288fc" +content-hash = "cb63112bfe7ee2b3fc194a422479f788c9a8027d445ea51a31f4ee20299beb53" diff --git a/pyproject.toml b/pyproject.toml index 116c034bc9..51396ca39b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,7 +126,7 @@ inputs = "*" Jinja2 = "*" lru-dict = "*" matplotlib = "*" -metadrive-simulator = { version = "0.4.2.2", markers = "platform_machine != 'aarch64'" } # no linux/aarch64 wheels for certain dependencies +metadrive-simulator = { version = "0.4.2.3", markers = "platform_machine != 'aarch64'" } # no linux/aarch64 wheels for certain dependencies mpld3 = "*" mypy = "*" myst-parser = "*" @@ -167,8 +167,8 @@ build-backend = "poetry.core.masonry.api" # https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml [tool.ruff] -select = ["E", "F", "W", "PIE", "C4", "ISC", "RUF008", "RUF100", "A", "B", "TID251"] -ignore = ["E741", "E402", "C408", "ISC003", "B027", "B024"] +lint.select = ["E", "F", "W", "PIE", "C4", "ISC", "RUF008", "RUF100", "A", "B", "TID251"] +lint.ignore = ["E741", "E402", "C408", "ISC003", "B027", "B024"] line-length = 160 target-version="py311" exclude = [ @@ -180,8 +180,8 @@ exclude = [ "teleoprtc_repo", "third_party", ] -flake8-implicit-str-concat.allow-multiline=false -[tool.ruff.flake8-tidy-imports.banned-api] +lint.flake8-implicit-str-concat.allow-multiline=false +[tool.ruff.lint.flake8-tidy-imports.banned-api] "selfdrive".msg = "Use openpilot.selfdrive" "common".msg = "Use openpilot.common" "system".msg = "Use openpilot.system" diff --git a/selfdrive/athena/athenad.py b/selfdrive/athena/athenad.py index 833bf841f5..9480d2b8ec 100755 --- a/selfdrive/athena/athenad.py +++ b/selfdrive/athena/athenad.py @@ -19,7 +19,8 @@ from dataclasses import asdict, dataclass, replace from datetime import datetime from functools import partial from queue import Queue -from typing import Callable, Dict, List, Optional, Set, Union, cast +from typing import cast +from collections.abc import Callable import requests from jsonrpc import JSONRPCResponseManager, dispatcher @@ -55,17 +56,17 @@ WS_FRAME_SIZE = 4096 NetworkType = log.DeviceState.NetworkType -UploadFileDict = Dict[str, Union[str, int, float, bool]] -UploadItemDict = Dict[str, Union[str, bool, int, float, Dict[str, str]]] +UploadFileDict = dict[str, str | int | float | bool] +UploadItemDict = dict[str, str | bool | int | float | dict[str, str]] -UploadFilesToUrlResponse = Dict[str, Union[int, List[UploadItemDict], List[str]]] +UploadFilesToUrlResponse = dict[str, int | list[UploadItemDict] | list[str]] @dataclass class UploadFile: fn: str url: str - headers: Dict[str, str] + headers: dict[str, str] allow_cellular: bool @classmethod @@ -77,9 +78,9 @@ class UploadFile: class UploadItem: path: str url: str - headers: Dict[str, str] + headers: dict[str, str] created_at: int - id: Optional[str] + id: str | None retry_count: int = 0 current: bool = False progress: float = 0 @@ -97,9 +98,9 @@ send_queue: Queue[str] = queue.Queue() upload_queue: Queue[UploadItem] = queue.Queue() low_priority_send_queue: Queue[str] = queue.Queue() log_recv_queue: Queue[str] = queue.Queue() -cancelled_uploads: Set[str] = set() +cancelled_uploads: set[str] = set() -cur_upload_items: Dict[int, Optional[UploadItem]] = {} +cur_upload_items: dict[int, UploadItem | None] = {} def strip_bz2_extension(fn: str) -> str: @@ -127,14 +128,14 @@ class UploadQueueCache: @staticmethod def cache(upload_queue: Queue[UploadItem]) -> None: try: - queue: List[Optional[UploadItem]] = list(upload_queue.queue) + queue: list[UploadItem | None] = list(upload_queue.queue) items = [asdict(i) for i in queue if i is not None and (i.id not in cancelled_uploads)] Params().put("AthenadUploadQueue", json.dumps(items)) except Exception: cloudlog.exception("athena.UploadQueueCache.cache.exception") -def handle_long_poll(ws: WebSocket, exit_event: Optional[threading.Event]) -> None: +def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None: end_event = threading.Event() threads = [ @@ -206,13 +207,17 @@ def retry_upload(tid: int, end_event: threading.Event, increase_count: bool = Tr break -def cb(sm, item, tid, sz: int, cur: int) -> None: +def cb(sm, item, tid, end_event: threading.Event, sz: int, cur: int) -> None: # Abort transfer if connection changed to metered after starting upload + # or if athenad is shutting down to re-connect the websocket sm.update(0) metered = sm['deviceState'].networkMetered if metered and (not item.allow_cellular): raise AbortTransferException + if end_event.is_set(): + raise AbortTransferException + cur_upload_items[tid] = replace(item, progress=cur / sz if sz else 1) @@ -252,7 +257,7 @@ def upload_handler(end_event: threading.Event) -> None: sz = -1 cloudlog.event("athena.upload_handler.upload_start", fn=fn, sz=sz, network_type=network_type, metered=metered, retry_count=item.retry_count) - response = _do_upload(item, partial(cb, sm, item, tid)) + response = _do_upload(item, partial(cb, sm, item, tid, end_event)) if response.status_code not in (200, 201, 401, 403, 412): cloudlog.event("athena.upload_handler.retry", status_code=response.status_code, fn=fn, sz=sz, network_type=network_type, metered=metered) @@ -274,7 +279,7 @@ def upload_handler(end_event: threading.Event) -> None: cloudlog.exception("athena.upload_handler.exception") -def _do_upload(upload_item: UploadItem, callback: Optional[Callable] = None) -> requests.Response: +def _do_upload(upload_item: UploadItem, callback: Callable | None = None) -> requests.Response: path = upload_item.path compress = False @@ -313,7 +318,7 @@ def getMessage(service: str, timeout: int = 1000) -> dict: @dispatcher.add_method -def getVersion() -> Dict[str, str]: +def getVersion() -> dict[str, str]: return { "version": get_version(), "remote": get_normalized_origin(), @@ -323,7 +328,7 @@ def getVersion() -> Dict[str, str]: @dispatcher.add_method -def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: Optional[str] = None, place_details: Optional[str] = None) -> Dict[str, int]: +def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: str | None = None, place_details: str | None = None) -> dict[str, int]: destination = { "latitude": latitude, "longitude": longitude, @@ -335,7 +340,7 @@ def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: Optiona return {"success": 1} -def scan_dir(path: str, prefix: str) -> List[str]: +def scan_dir(path: str, prefix: str) -> list[str]: files = [] # only walk directories that match the prefix # (glob and friends traverse entire dir tree) @@ -355,12 +360,12 @@ def scan_dir(path: str, prefix: str) -> List[str]: return files @dispatcher.add_method -def listDataDirectory(prefix='') -> List[str]: +def listDataDirectory(prefix='') -> list[str]: return scan_dir(Paths.log_root(), prefix) @dispatcher.add_method -def uploadFileToUrl(fn: str, url: str, headers: Dict[str, str]) -> UploadFilesToUrlResponse: +def uploadFileToUrl(fn: str, url: str, headers: dict[str, str]) -> UploadFilesToUrlResponse: # this is because mypy doesn't understand that the decorator doesn't change the return type response: UploadFilesToUrlResponse = uploadFilesToUrls([{ "fn": fn, @@ -371,11 +376,11 @@ def uploadFileToUrl(fn: str, url: str, headers: Dict[str, str]) -> UploadFilesTo @dispatcher.add_method -def uploadFilesToUrls(files_data: List[UploadFileDict]) -> UploadFilesToUrlResponse: +def uploadFilesToUrls(files_data: list[UploadFileDict]) -> UploadFilesToUrlResponse: files = map(UploadFile.from_dict, files_data) - items: List[UploadItemDict] = [] - failed: List[str] = [] + items: list[UploadItemDict] = [] + failed: list[str] = [] for file in files: if len(file.fn) == 0 or file.fn[0] == '/' or '..' in file.fn or len(file.url) == 0: failed.append(file.fn) @@ -414,13 +419,13 @@ def uploadFilesToUrls(files_data: List[UploadFileDict]) -> UploadFilesToUrlRespo @dispatcher.add_method -def listUploadQueue() -> List[UploadItemDict]: +def listUploadQueue() -> list[UploadItemDict]: items = list(upload_queue.queue) + list(cur_upload_items.values()) return [asdict(i) for i in items if (i is not None) and (i.id not in cancelled_uploads)] @dispatcher.add_method -def cancelUpload(upload_id: Union[str, List[str]]) -> Dict[str, Union[int, str]]: +def cancelUpload(upload_id: str | list[str]) -> dict[str, int | str]: if not isinstance(upload_id, list): upload_id = [upload_id] @@ -433,7 +438,7 @@ def cancelUpload(upload_id: Union[str, List[str]]) -> Dict[str, Union[int, str]] return {"success": 1} @dispatcher.add_method -def setRouteViewed(route: str) -> Dict[str, Union[int, str]]: +def setRouteViewed(route: str) -> dict[str, int | str]: # maintain a list of the last 10 routes viewed in connect params = Params() @@ -448,7 +453,7 @@ def setRouteViewed(route: str) -> Dict[str, Union[int, str]]: return {"success": 1} -def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> Dict[str, int]: +def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]: try: if local_port not in LOCAL_PORT_WHITELIST: raise Exception("Requested local port not whitelisted") @@ -482,7 +487,7 @@ def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local @dispatcher.add_method -def getPublicKey() -> Optional[str]: +def getPublicKey() -> str | None: if not os.path.isfile(Paths.persist_root() + '/comma/id_rsa.pub'): return None @@ -522,7 +527,7 @@ def getNetworks(): @dispatcher.add_method -def takeSnapshot() -> Optional[Union[str, Dict[str, str]]]: +def takeSnapshot() -> str | dict[str, str] | None: from openpilot.system.camerad.snapshot.snapshot import jpeg_write, snapshot ret = snapshot() if ret is not None: @@ -539,7 +544,7 @@ def takeSnapshot() -> Optional[Union[str, Dict[str, str]]]: raise Exception("not available while camerad is started") -def get_logs_to_send_sorted() -> List[str]: +def get_logs_to_send_sorted() -> list[str]: # TODO: scan once then use inotify to detect file creation/deletion curr_time = int(time.time()) logs = [] @@ -746,6 +751,9 @@ def ws_manage(ws: WebSocket, end_event: threading.Event) -> None: onroad_prev = onroad if sock is not None: + # While not sending data, onroad, we can expect to time out in 7 + (7 * 2) = 21s + # offroad, we can expect to time out in 30 + (10 * 3) = 60s + # FIXME: TCP_USER_TIMEOUT is effectively 2x for some reason (32s), so it's mostly unused sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, 16000 if onroad else 0) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7 if onroad else 30) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 7 if onroad else 10) @@ -759,7 +767,7 @@ def backoff(retries: int) -> int: return random.randrange(0, min(128, int(2 ** retries))) -def main(exit_event: Optional[threading.Event] = None): +def main(exit_event: threading.Event | None = None): try: set_core_affinity([0, 1, 2, 3]) except Exception: diff --git a/selfdrive/athena/registration.py b/selfdrive/athena/registration.py index 7db94c28c8..6574d9ac20 100755 --- a/selfdrive/athena/registration.py +++ b/selfdrive/athena/registration.py @@ -3,7 +3,6 @@ import time import json import jwt from pathlib import Path -from typing import Optional from datetime import datetime, timedelta from openpilot.common.api import api_get @@ -23,12 +22,12 @@ def is_registered_device() -> bool: return dongle not in (None, UNREGISTERED_DONGLE_ID) -def register(show_spinner=False) -> Optional[str]: +def register(show_spinner=False) -> str | None: params = Params() IMEI = params.get("IMEI", encoding='utf8') HardwareSerial = params.get("HardwareSerial", encoding='utf8') - dongle_id: Optional[str] = params.get("DongleId", encoding='utf8') + dongle_id: str | None = params.get("DongleId", encoding='utf8') needs_registration = None in (IMEI, HardwareSerial, dongle_id) pubkey = Path(Paths.persist_root()+"/comma/id_rsa.pub") @@ -48,8 +47,8 @@ def register(show_spinner=False) -> Optional[str]: # Block until we get the imei serial = HARDWARE.get_serial() start_time = time.monotonic() - imei1: Optional[str] = None - imei2: Optional[str] = None + imei1: str | None = None + imei2: str | None = None while imei1 is None and imei2 is None: try: imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1) diff --git a/selfdrive/athena/tests/test_athenad.py b/selfdrive/athena/tests/test_athenad.py index 2fecab1b1b..8d09661f01 100755 --- a/selfdrive/athena/tests/test_athenad.py +++ b/selfdrive/athena/tests/test_athenad.py @@ -12,7 +12,6 @@ import unittest from dataclasses import asdict, replace from datetime import datetime, timedelta from parameterized import parameterized -from typing import Optional from unittest import mock from websocket import ABNF @@ -97,7 +96,7 @@ class TestAthenadMethods(unittest.TestCase): break @staticmethod - def _create_file(file: str, parent: Optional[str] = None, data: bytes = b'') -> str: + def _create_file(file: str, parent: str | None = None, data: bytes = b'') -> str: fn = os.path.join(Paths.log_root() if parent is None else parent, file) os.makedirs(os.path.dirname(fn), exist_ok=True) with open(fn, 'wb') as f: @@ -233,6 +232,7 @@ class TestAthenadMethods(unittest.TestCase): time.sleep(0.1) # TODO: verify that upload actually succeeded + # TODO: also check that end_event and metered network raises AbortTransferException self.assertEqual(athenad.upload_queue.qsize(), 0) @parameterized.expand([(500, True), (412, False)]) diff --git a/selfdrive/athena/tests/test_athenad_ping.py b/selfdrive/athena/tests/test_athenad_ping.py index 5231b0475f..f56fcac8b5 100755 --- a/selfdrive/athena/tests/test_athenad_ping.py +++ b/selfdrive/athena/tests/test_athenad_ping.py @@ -3,7 +3,7 @@ import subprocess import threading import time import unittest -from typing import cast, Optional +from typing import cast from unittest import mock from openpilot.common.params import Params @@ -12,6 +12,8 @@ from openpilot.selfdrive.athena import athenad from openpilot.selfdrive.manager.helpers import write_onroad_params from openpilot.system.hardware import TICI +TIMEOUT_TOLERANCE = 20 # seconds + def wifi_radio(on: bool) -> None: if not TICI: @@ -27,8 +29,8 @@ class TestAthenadPing(unittest.TestCase): athenad: threading.Thread exit_event: threading.Event - def _get_ping_time(self) -> Optional[str]: - return cast(Optional[str], self.params.get("LastAthenaPingTime", encoding="utf-8")) + def _get_ping_time(self) -> str | None: + return cast(str | None, self.params.get("LastAthenaPingTime", encoding="utf-8")) def _clear_ping_time(self) -> None: self.params.remove("LastAthenaPingTime") @@ -55,7 +57,7 @@ class TestAthenadPing(unittest.TestCase): self.exit_event.set() self.athenad.join() - @mock.patch('openpilot.selfdrive.athena.athenad.create_connection', autospec=True) + @mock.patch('openpilot.selfdrive.athena.athenad.create_connection', new_callable=lambda: mock.MagicMock(wraps=athenad.create_connection)) def assertTimeout(self, reconnect_time: float, mock_create_connection: mock.MagicMock) -> None: self.athenad.start() @@ -63,7 +65,7 @@ class TestAthenadPing(unittest.TestCase): mock_create_connection.assert_called_once() mock_create_connection.reset_mock() - # check normal behaviour + # check normal behaviour, server pings on connection with self.subTest("Wi-Fi: receives ping"), Timeout(70, "no ping received"): while not self._received_ping(): time.sleep(0.1) @@ -92,12 +94,12 @@ class TestAthenadPing(unittest.TestCase): @unittest.skipIf(not TICI, "only run on desk") def test_offroad(self) -> None: write_onroad_params(False, self.params) - self.assertTimeout(100) # expect approx 90s + self.assertTimeout(60 + TIMEOUT_TOLERANCE) # based using TCP keepalive settings @unittest.skipIf(not TICI, "only run on desk") def test_onroad(self) -> None: write_onroad_params(True, self.params) - self.assertTimeout(30) # expect 20-30s + self.assertTimeout(21 + TIMEOUT_TOLERANCE) if __name__ == "__main__": diff --git a/selfdrive/boardd/boardd.cc b/selfdrive/boardd/boardd.cc index 9e2a960055..b2b59d3752 100644 --- a/selfdrive/boardd/boardd.cc +++ b/selfdrive/boardd/boardd.cc @@ -362,11 +362,11 @@ std::optional send_panda_states(PubMaster *pm, const std::vector ps.setHeartbeatLost((bool)(health.heartbeat_lost_pkt)); ps.setAlternativeExperience(health.alternative_experience_pkt); ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt)); - ps.setInterruptLoad(health.interrupt_load); + ps.setInterruptLoad(health.interrupt_load_pkt); ps.setFanPower(health.fan_power); ps.setFanStallCount(health.fan_stall_count); - ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid)); - ps.setSpiChecksumErrorCount(health.spi_checksum_error_count); + ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid_pkt)); + ps.setSpiChecksumErrorCount(health.spi_checksum_error_count_pkt); ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f); ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f); diff --git a/selfdrive/boardd/pandad.py b/selfdrive/boardd/pandad.py index 672678778b..988d1a2409 100755 --- a/selfdrive/boardd/pandad.py +++ b/selfdrive/boardd/pandad.py @@ -4,7 +4,7 @@ import os import usb1 import time import subprocess -from typing import List, NoReturn +from typing import NoReturn from functools import cmp_to_key from panda import Panda, PandaDFU, PandaProtocolMismatch, FW_PATH @@ -93,6 +93,11 @@ def main() -> NoReturn: cloudlog.event("pandad.flash_and_connect", count=count) params.remove("PandaSignatures") + # TODO: remove this in the next AGNOS + # wait until USB is up before counting + if time.monotonic() < 25.: + no_internal_panda_count = 0 + # Handle missing internal panda if no_internal_panda_count > 0: if no_internal_panda_count == 3: @@ -119,7 +124,7 @@ def main() -> NoReturn: cloudlog.info(f"{len(panda_serials)} panda(s) found, connecting - {panda_serials}") # Flash pandas - pandas: List[Panda] = [] + pandas: list[Panda] = [] for serial in panda_serials: pandas.append(flash_panda(serial)) diff --git a/selfdrive/boardd/tests/test_boardd_loopback.py b/selfdrive/boardd/tests/test_boardd_loopback.py index dfce0e3710..148ce9a25d 100755 --- a/selfdrive/boardd/tests/test_boardd_loopback.py +++ b/selfdrive/boardd/tests/test_boardd_loopback.py @@ -32,7 +32,7 @@ class TestBoardd(unittest.TestCase): with Timeout(90, "boardd didn't start"): sm = messaging.SubMaster(['pandaStates']) - while sm.rcv_frame['pandaStates'] < 1 or len(sm['pandaStates']) == 0 or \ + while sm.recv_frame['pandaStates'] < 1 or len(sm['pandaStates']) == 0 or \ any(ps.pandaType == log.PandaState.PandaType.unknown for ps in sm['pandaStates']): sm.update(1000) diff --git a/selfdrive/car/__init__.py b/selfdrive/car/__init__.py index c90ae50ab9..7544796b93 100644 --- a/selfdrive/car/__init__.py +++ b/selfdrive/car/__init__.py @@ -1,11 +1,13 @@ # functions common among cars from collections import namedtuple -from typing import Dict, List, Optional +from dataclasses import dataclass +from enum import ReprEnum import capnp from cereal import car from openpilot.common.numpy_fast import clip, interp +from openpilot.selfdrive.car.docs_definitions import CarInfo # kg of standard extra cargo to count for drive, gas, etc... @@ -24,9 +26,9 @@ def apply_hysteresis(val: float, val_steady: float, hyst_gap: float) -> float: return val_steady -def create_button_events(cur_btn: int, prev_btn: int, buttons_dict: Dict[int, capnp.lib.capnp._EnumModule], - unpressed_btn: int = 0) -> List[capnp.lib.capnp._DynamicStructBuilder]: - events: List[capnp.lib.capnp._DynamicStructBuilder] = [] +def create_button_events(cur_btn: int, prev_btn: int, buttons_dict: dict[int, capnp.lib.capnp._EnumModule], + unpressed_btn: int = 0) -> list[capnp.lib.capnp._DynamicStructBuilder]: + events: list[capnp.lib.capnp._DynamicStructBuilder] = [] if cur_btn == prev_btn: return events @@ -73,7 +75,10 @@ def scale_tire_stiffness(mass, wheelbase, center_to_front, tire_stiffness_factor return tire_stiffness_front, tire_stiffness_rear -def dbc_dict(pt_dbc, radar_dbc, chassis_dbc=None, body_dbc=None) -> Dict[str, str]: +DbcDict = dict[str, str] + + +def dbc_dict(pt_dbc, radar_dbc, chassis_dbc=None, body_dbc=None) -> DbcDict: return {'pt': pt_dbc, 'radar': radar_dbc, 'chassis': chassis_dbc, 'body': body_dbc} @@ -208,7 +213,7 @@ def get_safety_config(safety_model, safety_param = None): class CanBusBase: offset: int - def __init__(self, CP, fingerprint: Optional[Dict[int, Dict[int, int]]]) -> None: + def __init__(self, CP, fingerprint: dict[int, dict[int, int]] | None) -> None: if CP is None: assert fingerprint is not None num = max([k for k, v in fingerprint.items() if len(v)], default=0) // 4 + 1 @@ -236,3 +241,43 @@ class CanSignalRateCalculator: self.previous_value = current_value return self.rate + + +CarInfos = CarInfo | list[CarInfo] + + +@dataclass +class CarSpecs: + mass: float + wheelbase: float + steerRatio: float + + +@dataclass(order=True) +class PlatformConfig: + platform_str: str + car_info: CarInfos + dbc_dict: DbcDict + + specs: CarSpecs | None = None + + def __hash__(self) -> int: + return hash(self.platform_str) + + +class Platforms(str, ReprEnum): + config: PlatformConfig + + def __new__(cls, platform_config: PlatformConfig): + member = str.__new__(cls, platform_config.platform_str) + member.config = platform_config + member._value_ = platform_config.platform_str + return member + + @classmethod + def create_dbc_map(cls) -> dict[str, DbcDict]: + return {p: p.config.dbc_dict for p in cls} + + @classmethod + def create_carinfo_map(cls) -> dict[str, CarInfos]: + return {p: p.config.car_info for p in cls} diff --git a/selfdrive/car/body/values.py b/selfdrive/car/body/values.py index 33119bf0fd..441905f28b 100644 --- a/selfdrive/car/body/values.py +++ b/selfdrive/car/body/values.py @@ -1,8 +1,5 @@ -from enum import StrEnum -from typing import Dict - from cereal import car -from openpilot.selfdrive.car import dbc_dict +from openpilot.selfdrive.car import PlatformConfig, Platforms, dbc_dict from openpilot.selfdrive.car.docs_definitions import CarInfo from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries @@ -22,13 +19,12 @@ class CarControllerParams: pass -class CAR(StrEnum): - BODY = "COMMA BODY" - - -CAR_INFO: Dict[str, CarInfo] = { - CAR.BODY: CarInfo("comma body", package="All"), -} +class CAR(Platforms): + BODY = PlatformConfig( + "COMMA BODY", + CarInfo("comma body", package="All"), + dbc_dict('comma_body', None), + ) FW_QUERY_CONFIG = FwQueryConfig( @@ -41,7 +37,5 @@ FW_QUERY_CONFIG = FwQueryConfig( ], ) - -DBC = { - CAR.BODY: dbc_dict('comma_body', None), -} +CAR_INFO = CAR.create_carinfo_map() +DBC = CAR.create_dbc_map() diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index 7e96008149..fe4c0e885c 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -1,6 +1,6 @@ import os import time -from typing import Callable, Dict, List, Optional, Tuple +from collections.abc import Callable from cereal import car from openpilot.common.params import Params @@ -63,7 +63,7 @@ def load_interfaces(brand_names): return ret -def _get_interface_names() -> Dict[str, List[str]]: +def _get_interface_names() -> dict[str, list[str]]: # returns a dict of brand name and its respective models brand_names = {} for brand_name, brand_models in get_interface_attr("CAR").items(): @@ -77,7 +77,7 @@ interface_names = _get_interface_names() interfaces = load_interfaces(interface_names) -def can_fingerprint(next_can: Callable) -> Tuple[Optional[str], Dict[int, dict]]: +def can_fingerprint(next_can: Callable) -> tuple[str | None, dict[int, dict]]: finger = gen_empty_fingerprint() candidate_cars = {i: all_legacy_fingerprint_cars() for i in [0, 1]} # attempt fingerprint on both bus 0 and 1 frame = 0 @@ -141,9 +141,10 @@ def fingerprint(logcan, sendcan, num_pandas): cached = True else: cloudlog.warning("Getting VIN & FW versions") - # enable OBD multiplexing for Vin query, also allows time for sendcan subscriber to connect + # enable OBD multiplexing for VIN query + # NOTE: this takes ~0.1s and is relied on to allow sendcan subscriber to connect in time set_obd_multiplexing(params, True) - # Vin query only reliably works through OBDII + # VIN query only reliably works through OBDII vin_rx_addr, vin_rx_bus, vin = get_vin(logcan, sendcan, (0, 1)) ecu_rx_addrs = get_present_ecus(logcan, sendcan, num_pandas=num_pandas) car_fw = get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, num_pandas=num_pandas) @@ -161,7 +162,7 @@ def fingerprint(logcan, sendcan, num_pandas): cloudlog.warning("VIN %s", vin) params.put("CarVin", vin) - # disable OBD multiplexing for potential ECU knockouts + # disable OBD multiplexing for CAN fingerprinting and potential ECU knockouts set_obd_multiplexing(params, False) params.put_bool("FirmwareQueryDone", True) diff --git a/selfdrive/car/chrysler/fingerprints.py b/selfdrive/car/chrysler/fingerprints.py index 0de49f11b9..1df514e79b 100644 --- a/selfdrive/car/chrysler/fingerprints.py +++ b/selfdrive/car/chrysler/fingerprints.py @@ -67,6 +67,7 @@ FW_VERSIONS = { (Ecu.engine, 0x7e0, None): [ b'68267018AO ', b'68267020AJ ', + b'68303534AJ ', b'68340762AD ', b'68340764AD ', b'68352652AE ', @@ -299,6 +300,7 @@ FW_VERSIONS = { CAR.JEEP_GRAND_CHEROKEE_2019: { (Ecu.combinationMeter, 0x742, None): [ b'68402703AB', + b'68402704AB', b'68402708AB', b'68402971AD', b'68454144AD', @@ -326,6 +328,7 @@ FW_VERSIONS = { (Ecu.eps, 0x75a, None): [ b'68417279AA', b'68417280AA', + b'68417281AA', b'68453431AA', b'68453433AA', b'68453435AA', @@ -336,6 +339,7 @@ FW_VERSIONS = { (Ecu.engine, 0x7e0, None): [ b'05035674AB ', b'68412635AG ', + b'68412660AD ', b'68422860AB', b'68449435AE ', b'68496223AA ', @@ -346,6 +350,7 @@ FW_VERSIONS = { (Ecu.transmission, 0x7e1, None): [ b'05035707AA', b'68419672AC', + b'68419678AB', b'68423905AB', b'68449258AC', b'68495807AA', @@ -396,6 +401,7 @@ FW_VERSIONS = { b'68527383AD', b'68527387AE', b'68527403AC', + b'68546047AF', b'68631938AA', b'68631942AA', ], @@ -475,6 +481,7 @@ FW_VERSIONS = { b'05149591AD ', b'05149591AE ', b'05149592AE ', + b'05149599AE ', b'05149600AD ', b'05149605AE ', b'05149846AA ', @@ -606,4 +613,34 @@ FW_VERSIONS = { b'M2421132MB', ], }, + CAR.DODGE_DURANGO: { + (Ecu.combinationMeter, 0x742, None): [ + b'68454261AD', + b'68471535AE', + ], + (Ecu.srs, 0x744, None): [ + b'68355362AB', + b'68492238AD', + ], + (Ecu.abs, 0x747, None): [ + b'68408639AD', + b'68499978AB', + ], + (Ecu.fwdRadar, 0x753, None): [ + b'68440581AE', + b'68456722AC', + ], + (Ecu.eps, 0x75a, None): [ + b'68453435AA', + b'68498477AA', + ], + (Ecu.engine, 0x7e0, None): [ + b'05035786AE ', + b'68449476AE ', + ], + (Ecu.transmission, 0x7e1, None): [ + b'05035826AC', + b'68449265AC', + ], + }, } diff --git a/selfdrive/car/chrysler/interface.py b/selfdrive/car/chrysler/interface.py index 10cdd0fd14..32a4f5dfcf 100755 --- a/selfdrive/car/chrysler/interface.py +++ b/selfdrive/car/chrysler/interface.py @@ -28,13 +28,13 @@ class CarInterface(CarInterfaceBase): CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) if candidate not in RAM_CARS: # Newer FW versions standard on the following platforms, or flashed by a dealer onto older platforms have a higher minimum steering speed. - new_eps_platform = candidate in (CAR.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020, CAR.JEEP_GRAND_CHEROKEE_2019) + new_eps_platform = candidate in (CAR.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020, CAR.JEEP_GRAND_CHEROKEE_2019, CAR.DODGE_DURANGO) new_eps_firmware = any(fw.ecu == 'eps' and fw.fwVersion[:4] >= b"6841" for fw in car_fw) if new_eps_platform or new_eps_firmware: ret.flags |= ChryslerFlags.HIGHER_MIN_STEERING_SPEED.value # Chrysler - if candidate in (CAR.PACIFICA_2017_HYBRID, CAR.PACIFICA_2018, CAR.PACIFICA_2018_HYBRID, CAR.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020): + if candidate in (CAR.PACIFICA_2017_HYBRID, CAR.PACIFICA_2018, CAR.PACIFICA_2018_HYBRID, CAR.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020, CAR.DODGE_DURANGO): ret.mass = 2242. ret.wheelbase = 3.089 ret.steerRatio = 16.2 # Pacifica Hybrid 2017 @@ -80,6 +80,7 @@ class CarInterface(CarInterfaceBase): if ret.flags & ChryslerFlags.HIGHER_MIN_STEERING_SPEED: # TODO: allow these cars to steer down to 13 m/s if already engaged. + # TODO: Durango 2020 may be able to steer to zero once above 38 kph ret.minSteerSpeed = 17.5 # m/s 17 on the way up, 13 on the way down once engaged. ret.centerToFront = ret.wheelbase * 0.44 diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index 34e602562a..a7eec8fe5a 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -1,6 +1,5 @@ from enum import IntFlag, StrEnum from dataclasses import dataclass, field -from typing import Dict, List, Optional, Union from cereal import car from panda.python import uds @@ -23,6 +22,9 @@ class CAR(StrEnum): PACIFICA_2018 = "CHRYSLER PACIFICA 2018" PACIFICA_2020 = "CHRYSLER PACIFICA 2020" + # Dodge + DODGE_DURANGO = "DODGE DURANGO 2021" + # Jeep JEEP_GRAND_CHEROKEE = "JEEP GRAND CHEROKEE V6 2018" # includes 2017 Trailhawk JEEP_GRAND_CHEROKEE_2019 = "JEEP GRAND CHEROKEE 2019" # includes 2020 Trailhawk @@ -63,7 +65,7 @@ class ChryslerCarInfo(CarInfo): car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.fca])) -CAR_INFO: Dict[str, Optional[Union[ChryslerCarInfo, List[ChryslerCarInfo]]]] = { +CAR_INFO: dict[str, ChryslerCarInfo | list[ChryslerCarInfo] | None] = { CAR.PACIFICA_2017_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2017"), CAR.PACIFICA_2018_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2018"), CAR.PACIFICA_2019_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2019-23"), @@ -74,6 +76,7 @@ CAR_INFO: Dict[str, Optional[Union[ChryslerCarInfo, List[ChryslerCarInfo]]]] = { ], CAR.JEEP_GRAND_CHEROKEE: ChryslerCarInfo("Jeep Grand Cherokee 2016-18", video_link="https://www.youtube.com/watch?v=eLR9o2JkuRk"), CAR.JEEP_GRAND_CHEROKEE_2019: ChryslerCarInfo("Jeep Grand Cherokee 2019-21", video_link="https://www.youtube.com/watch?v=jBe4lWnRSu4"), + CAR.DODGE_DURANGO: ChryslerCarInfo("Dodge Durango 2020-21"), CAR.RAM_1500: ChryslerCarInfo("Ram 1500 2019-24", car_parts=CarParts.common([CarHarness.ram])), CAR.RAM_HD: [ ChryslerCarInfo("Ram 2500 2020-24", car_parts=CarParts.common([CarHarness.ram])), @@ -128,6 +131,7 @@ DBC = { CAR.PACIFICA_2020: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), CAR.PACIFICA_2018_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), CAR.PACIFICA_2019_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), + CAR.DODGE_DURANGO: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), CAR.JEEP_GRAND_CHEROKEE: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), CAR.JEEP_GRAND_CHEROKEE_2019: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), CAR.RAM_1500: dbc_dict('chrysler_ram_dt_generated', None), diff --git a/selfdrive/car/docs.py b/selfdrive/car/docs.py index 8475a69d8a..caa79a8f87 100755 --- a/selfdrive/car/docs.py +++ b/selfdrive/car/docs.py @@ -5,7 +5,6 @@ import jinja2 import os from enum import Enum from natsort import natsorted -from typing import Dict, List from cereal import car from openpilot.common.basedir import BASEDIR @@ -14,7 +13,7 @@ from openpilot.selfdrive.car.docs_definitions import CarInfo, Column, CommonFoot from openpilot.selfdrive.car.car_helpers import interfaces, get_interface_attr -def get_all_footnotes() -> Dict[Enum, int]: +def get_all_footnotes() -> dict[Enum, int]: all_footnotes = list(CommonFootnote) for footnotes in get_interface_attr("Footnote", ignore_none=True).values(): all_footnotes.extend(footnotes) @@ -25,8 +24,8 @@ CARS_MD_OUT = os.path.join(BASEDIR, "docs", "CARS.md") CARS_MD_TEMPLATE = os.path.join(BASEDIR, "selfdrive", "car", "CARS_template.md") -def get_all_car_info() -> List[CarInfo]: - all_car_info: List[CarInfo] = [] +def get_all_car_info() -> list[CarInfo]: + all_car_info: list[CarInfo] = [] footnotes = get_all_footnotes() for model, car_info in get_interface_attr("CAR_INFO", combine_brands=True).items(): # If available, uses experimental longitudinal limits for the docs @@ -47,19 +46,19 @@ def get_all_car_info() -> List[CarInfo]: all_car_info.append(_car_info) # Sort cars by make and model + year - sorted_cars: List[CarInfo] = natsorted(all_car_info, key=lambda car: car.name.lower()) + sorted_cars: list[CarInfo] = natsorted(all_car_info, key=lambda car: car.name.lower()) return sorted_cars -def group_by_make(all_car_info: List[CarInfo]) -> Dict[str, List[CarInfo]]: +def group_by_make(all_car_info: list[CarInfo]) -> dict[str, list[CarInfo]]: sorted_car_info = defaultdict(list) for car_info in all_car_info: sorted_car_info[car_info.make].append(car_info) return dict(sorted_car_info) -def generate_cars_md(all_car_info: List[CarInfo], template_fn: str) -> str: - with open(template_fn, "r") as f: +def generate_cars_md(all_car_info: list[CarInfo], template_fn: str) -> str: + with open(template_fn) as f: template = jinja2.Template(f.read(), trim_blocks=True, lstrip_blocks=True) footnotes = [fn.value.text for fn in get_all_footnotes()] diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index f2ce743607..03ed1f32cb 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -3,7 +3,6 @@ from collections import namedtuple import copy from dataclasses import dataclass, field from enum import Enum -from typing import Dict, List, Optional, Tuple, Union from cereal import car from openpilot.common.conversions import Conversions as CV @@ -35,7 +34,7 @@ class Star(Enum): @dataclass class BasePart: name: str - parts: List[Enum] = field(default_factory=list) + parts: list[Enum] = field(default_factory=list) def all_parts(self): # Recursively get all parts @@ -76,7 +75,7 @@ class Accessory(EnumBase): @dataclass class BaseCarHarness(BasePart): - parts: List[Enum] = field(default_factory=lambda: [Accessory.harness_box, Accessory.comma_power_v2, Cable.rj45_cable_7ft]) + parts: list[Enum] = field(default_factory=lambda: [Accessory.harness_box, Accessory.comma_power_v2, Cable.rj45_cable_7ft]) has_connector: bool = True # without are hidden on the harness connector page @@ -149,18 +148,18 @@ class PartType(Enum): tool = Tool -DEFAULT_CAR_PARTS: List[EnumBase] = [Device.threex] +DEFAULT_CAR_PARTS: list[EnumBase] = [Device.threex] @dataclass class CarParts: - parts: List[EnumBase] = field(default_factory=list) + parts: list[EnumBase] = field(default_factory=list) def __call__(self): return copy.deepcopy(self) @classmethod - def common(cls, add: Optional[List[EnumBase]] = None, remove: Optional[List[EnumBase]] = None): + def common(cls, add: list[EnumBase] | None = None, remove: list[EnumBase] | None = None): p = [part for part in (add or []) + DEFAULT_CAR_PARTS if part not in (remove or [])] return cls(p) @@ -186,7 +185,7 @@ class CommonFootnote(Enum): Column.LONGITUDINAL) -def get_footnotes(footnotes: List[Enum], column: Column) -> List[Enum]: +def get_footnotes(footnotes: list[Enum], column: Column) -> list[Enum]: # Returns applicable footnotes given current column return [fn for fn in footnotes if fn.value.column == column] @@ -209,7 +208,7 @@ def get_year_list(years): return years_list -def split_name(name: str) -> Tuple[str, str, str]: +def split_name(name: str) -> tuple[str, str, str]: make, model = name.split(" ", 1) years = "" match = re.search(MODEL_YEARS_RE, model) @@ -233,13 +232,13 @@ class CarInfo: # the minimum compatibility requirements for this model, regardless # of market. can be a package, trim, or list of features - requirements: Optional[str] = None + requirements: str | None = None - video_link: Optional[str] = None - footnotes: List[Enum] = field(default_factory=list) - min_steer_speed: Optional[float] = None - min_enable_speed: Optional[float] = None - auto_resume: Optional[bool] = None + video_link: str | None = None + footnotes: list[Enum] = field(default_factory=list) + min_steer_speed: float | None = None + min_enable_speed: float | None = None + auto_resume: bool | None = None # all the parts needed for the supported car car_parts: CarParts = field(default_factory=CarParts) @@ -248,7 +247,7 @@ class CarInfo: 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_fingerprint = CP.carFingerprint @@ -293,7 +292,7 @@ class CarInfo: if len(tools_docs): hardware_col += f'
Tools{display_func(tools_docs)}
' - self.row: Dict[Enum, Union[str, Star]] = { + self.row: dict[Enum, str | Star] = { Column.MAKE: self.make, Column.MODEL: self.model, Column.PACKAGE: self.package, @@ -352,7 +351,7 @@ class CarInfo: raise Exception(f"This notCar does not have a detail sentence: {CP.carFingerprint}") def get_column(self, column: Column, star_icon: str, video_icon: str, footnote_tag: str) -> str: - item: Union[str, Star] = self.row[column] + item: str | Star = self.row[column] if isinstance(item, Star): item = star_icon.format(item.value) elif column == Column.MODEL and len(self.years): diff --git a/selfdrive/car/ecu_addrs.py b/selfdrive/car/ecu_addrs.py index 13f7926def..6d6fa333a5 100755 --- a/selfdrive/car/ecu_addrs.py +++ b/selfdrive/car/ecu_addrs.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import capnp import time -from typing import Optional, Set import cereal.messaging as messaging from panda.python.uds import SERVICE_TYPE @@ -20,7 +19,7 @@ def make_tester_present_msg(addr, bus, subaddr=None): return make_can_msg(addr, bytes(dat), bus) -def is_tester_present_response(msg: capnp.lib.capnp._DynamicStructReader, subaddr: Optional[int] = None) -> bool: +def is_tester_present_response(msg: capnp.lib.capnp._DynamicStructReader, subaddr: int | None = None) -> bool: # ISO-TP messages are always padded to 8 bytes # tester present response is always a single frame dat_offset = 1 if subaddr is not None else 0 @@ -34,16 +33,16 @@ def is_tester_present_response(msg: capnp.lib.capnp._DynamicStructReader, subadd return False -def get_all_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, bus: int, timeout: float = 1, debug: bool = True) -> Set[EcuAddrBusType]: +def get_all_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, bus: int, timeout: float = 1, debug: bool = True) -> set[EcuAddrBusType]: addr_list = [0x700 + i for i in range(256)] + [0x18da00f1 + (i << 8) for i in range(256)] - queries: Set[EcuAddrBusType] = {(addr, None, bus) for addr in addr_list} + queries: set[EcuAddrBusType] = {(addr, None, bus) for addr in addr_list} responses = queries return get_ecu_addrs(logcan, sendcan, queries, responses, timeout=timeout, debug=debug) -def get_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, queries: Set[EcuAddrBusType], - responses: Set[EcuAddrBusType], timeout: float = 1, debug: bool = False) -> Set[EcuAddrBusType]: - ecu_responses: Set[EcuAddrBusType] = set() # set((addr, subaddr, bus),) +def get_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, queries: set[EcuAddrBusType], + responses: set[EcuAddrBusType], timeout: float = 1, debug: bool = False) -> set[EcuAddrBusType]: + ecu_responses: set[EcuAddrBusType] = set() # set((addr, subaddr, bus),) try: msgs = [make_tester_present_msg(addr, bus, subaddr) for addr, subaddr, bus in queries] diff --git a/selfdrive/car/ford/carstate.py b/selfdrive/car/ford/carstate.py index 9230d16ef9..34006e8da4 100644 --- a/selfdrive/car/ford/carstate.py +++ b/selfdrive/car/ford/carstate.py @@ -18,17 +18,10 @@ class CarState(CarStateBase): self.shifter_values = can_define.dv["Gear_Shift_by_Wire_FD1"]["TrnRng_D_RqGsm"] self.vehicle_sensors_valid = False - self.unsupported_platform = False def update(self, cp, cp_cam): ret = car.CarState.new_message() - # Ford Q3 hybrid variants experience a bug where a message from the PCM sends invalid checksums, - # this must be root-caused before enabling support. Ford Q4 hybrids do not have this problem. - # TrnAin_Tq_Actl and its quality flag are only set on ICE platform variants - self.unsupported_platform = (cp.vl["VehicleOperatingModes"]["TrnAinTq_D_Qf"] == 0 and - self.CP.carFingerprint not in CANFD_CAR) - # Occasionally on startup, the ABS module recalibrates the steering pinion offset, so we need to block engagement # The vehicle usually recovers out of this state within a minute of normal driving self.vehicle_sensors_valid = cp.vl["SteeringPinion_Data"]["StePinCompAnEst_D_Qf"] == 3 @@ -54,7 +47,7 @@ class CarState(CarStateBase): ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > CarControllerParams.STEER_DRIVER_ALLOWANCE, 5) ret.steerFaultTemporary = cp.vl["EPAS_INFO"]["EPAS_Failure"] == 1 ret.steerFaultPermanent = cp.vl["EPAS_INFO"]["EPAS_Failure"] in (2, 3) - # ret.espDisabled = False # TODO: find traction control signal + ret.espDisabled = cp.vl["Cluster_Info1_FD1"]["DrvSlipCtlMde_D_Rq"] != 0 # 0 is default mode if self.CP.carFingerprint in CANFD_CAR: # this signal is always 0 on non-CAN FD cars diff --git a/selfdrive/car/ford/interface.py b/selfdrive/car/ford/interface.py index cc013fb54b..685a2a27ad 100644 --- a/selfdrive/car/ford/interface.py +++ b/selfdrive/car/ford/interface.py @@ -109,8 +109,6 @@ class CarInterface(CarInterfaceBase): events = self.create_common_events(ret, extra_gears=[GearShifter.manumatic]) if not self.CS.vehicle_sensors_valid: events.add(car.CarEvent.EventName.vehicleSensorsInvalid) - if self.CS.unsupported_platform: - events.add(car.CarEvent.EventName.startupNoControl) ret.events = events.to_msg() diff --git a/selfdrive/car/ford/tests/test_ford.py b/selfdrive/car/ford/tests/test_ford.py index dff29629f6..fb5d07f4bf 100755 --- a/selfdrive/car/ford/tests/test_ford.py +++ b/selfdrive/car/ford/tests/test_ford.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import unittest from parameterized import parameterized -from typing import Dict, Iterable, Optional, Tuple +from collections.abc import Iterable import capnp @@ -50,7 +50,7 @@ class TestFordFW(unittest.TestCase): self.assertIsNone(subaddr, "Unexpected ECU subaddress") @parameterized.expand(FW_VERSIONS.items()) - def test_fw_versions(self, car_model: str, fw_versions: Dict[Tuple[capnp.lib.capnp._EnumModule, int, Optional[int]], Iterable[bytes]]): + def test_fw_versions(self, car_model: str, fw_versions: dict[tuple[capnp.lib.capnp._EnumModule, int, int | None], Iterable[bytes]]): for (ecu, addr, subaddr), fws in fw_versions.items(): self.assertIn(ecu, ECU_FW_CORE, "Unexpected ECU") self.assertEqual(addr, ECU_ADDRESSES[ecu], "ECU address mismatch") diff --git a/selfdrive/car/ford/values.py b/selfdrive/car/ford/values.py index c080e02299..3ad39d715f 100644 --- a/selfdrive/car/ford/values.py +++ b/selfdrive/car/ford/values.py @@ -1,7 +1,6 @@ from collections import defaultdict from dataclasses import dataclass from enum import Enum, StrEnum -from typing import Dict, List, Union from cereal import car from openpilot.selfdrive.car import AngleRateLimit, dbc_dict @@ -59,7 +58,7 @@ class RADAR: DELPHI_MRR = 'FORD_CADS' -DBC: Dict[str, Dict[str, str]] = defaultdict(lambda: dbc_dict("ford_lincoln_base_pt", RADAR.DELPHI_MRR)) +DBC: dict[str, dict[str, str]] = defaultdict(lambda: dbc_dict("ford_lincoln_base_pt", RADAR.DELPHI_MRR)) # F-150 radar is not yet supported DBC[CAR.F_150_MK14] = dbc_dict("ford_lincoln_base_pt", None) @@ -87,23 +86,37 @@ class FordCarInfo(CarInfo): self.car_parts = CarParts([Device.threex, harness]) -CAR_INFO: Dict[str, Union[CarInfo, List[CarInfo]]] = { +CAR_INFO: dict[str, CarInfo | list[CarInfo]] = { CAR.BRONCO_SPORT_MK1: FordCarInfo("Ford Bronco Sport 2021-22"), CAR.ESCAPE_MK4: [ FordCarInfo("Ford Escape 2020-22"), + FordCarInfo("Ford Escape Hybrid 2020-22"), + FordCarInfo("Ford Escape Plug-in Hybrid 2020-22"), FordCarInfo("Ford Kuga 2020-22", "Adaptive Cruise Control with Lane Centering"), + FordCarInfo("Ford Kuga Hybrid 2020-22", "Adaptive Cruise Control with Lane Centering"), + FordCarInfo("Ford Kuga Plug-in Hybrid 2020-22", "Adaptive Cruise Control with Lane Centering"), ], CAR.EXPLORER_MK6: [ FordCarInfo("Ford Explorer 2020-23"), - FordCarInfo("Lincoln Aviator 2020-21", "Co-Pilot360 Plus"), + FordCarInfo("Ford Explorer Hybrid 2020-23"), # Limited and Platinum only + FordCarInfo("Lincoln Aviator 2020-23", "Co-Pilot360 Plus"), + FordCarInfo("Lincoln Aviator Plug-in Hybrid 2020-23", "Co-Pilot360 Plus"), # Grand Touring only + ], + CAR.F_150_MK14: [ + FordCarInfo("Ford F-150 2023", "Co-Pilot360 Active 2.0"), + FordCarInfo("Ford F-150 Hybrid 2023", "Co-Pilot360 Active 2.0"), ], - CAR.F_150_MK14: FordCarInfo("Ford F-150 2023", "Co-Pilot360 Active 2.0"), CAR.F_150_LIGHTNING_MK1: FordCarInfo("Ford F-150 Lightning 2021-23", "Co-Pilot360 Active 2.0"), CAR.MUSTANG_MACH_E_MK1: FordCarInfo("Ford Mustang Mach-E 2021-23", "Co-Pilot360 Active 2.0"), - CAR.FOCUS_MK4: FordCarInfo("Ford Focus 2018", "Adaptive Cruise Control with Lane Centering", footnotes=[Footnote.FOCUS]), + CAR.FOCUS_MK4: [ + FordCarInfo("Ford Focus 2018", "Adaptive Cruise Control with Lane Centering", footnotes=[Footnote.FOCUS]), + FordCarInfo("Ford Focus Hybrid 2018", "Adaptive Cruise Control with Lane Centering", footnotes=[Footnote.FOCUS]), # mHEV only + ], CAR.MAVERICK_MK1: [ FordCarInfo("Ford Maverick 2022", "LARIAT Luxury"), + FordCarInfo("Ford Maverick Hybrid 2022", "LARIAT Luxury"), FordCarInfo("Ford Maverick 2023", "Co-Pilot360 Assist"), + FordCarInfo("Ford Maverick Hybrid 2023", "Co-Pilot360 Assist"), ], } @@ -111,6 +124,11 @@ FW_QUERY_CONFIG = FwQueryConfig( requests=[ # CAN and CAN FD queries are combined. # FIXME: For CAN FD, ECUs respond with frames larger than 8 bytes on the powertrain bus + Request( + [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST], + [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE], + logging=True, + ), Request( [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST], [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE], diff --git a/selfdrive/car/fw_query_definitions.py b/selfdrive/car/fw_query_definitions.py index 1d6104574e..80492b4177 100755 --- a/selfdrive/car/fw_query_definitions.py +++ b/selfdrive/car/fw_query_definitions.py @@ -3,19 +3,21 @@ import capnp import copy from dataclasses import dataclass, field import struct -from typing import Callable, Dict, List, Optional, Set, Tuple +from collections.abc import Callable import panda.python.uds as uds -AddrType = Tuple[int, Optional[int]] -EcuAddrBusType = Tuple[int, Optional[int], int] -EcuAddrSubAddr = Tuple[int, int, Optional[int]] +AddrType = tuple[int, int | None] +EcuAddrBusType = tuple[int, int | None, int] +EcuAddrSubAddr = tuple[int, int, int | None] -LiveFwVersions = Dict[AddrType, Set[bytes]] -OfflineFwVersions = Dict[str, Dict[EcuAddrSubAddr, List[bytes]]] +LiveFwVersions = dict[AddrType, set[bytes]] +OfflineFwVersions = dict[str, dict[EcuAddrSubAddr, list[bytes]]] # A global list of addresses we will only ever consider for VIN responses -STANDARD_VIN_ADDRS = [0x7e0, 0x7e2, 0x760, 0x18da10f1, 0x18da0ef1] # engine, hybrid controller, Ford abs, 29-bit engine, PGM-FI +# engine, hybrid controller, Ford abs, Hyundai CAN FD cluster, 29-bit engine, PGM-FI +# TODO: move these to each brand's FW query config +STANDARD_VIN_ADDRS = [0x7e0, 0x7e2, 0x760, 0x7c6, 0x18da10f1, 0x18da0ef1] def p16(val): @@ -63,12 +65,15 @@ class StdQueries: GM_VIN_REQUEST = b'\x1a\x90' GM_VIN_RESPONSE = b'\x5a\x90' + KWP_VIN_REQUEST = b'\x21\x81' + KWP_VIN_RESPONSE = b'\x61\x81' + @dataclass class Request: - request: List[bytes] - response: List[bytes] - whitelist_ecus: List[int] = field(default_factory=list) + request: list[bytes] + response: list[bytes] + whitelist_ecus: list[int] = field(default_factory=list) rx_offset: int = 0x8 bus: int = 1 # Whether this query should be run on the first auxiliary panda (CAN FD cars for example) @@ -81,15 +86,15 @@ class Request: @dataclass class FwQueryConfig: - requests: List[Request] + requests: list[Request] # TODO: make this automatic and remove hardcoded lists, or do fingerprinting with ecus # Overrides and removes from essential ecus for specific models and ecus (exact matching) - non_essential_ecus: Dict[capnp.lib.capnp._EnumModule, List[str]] = field(default_factory=dict) + non_essential_ecus: dict[capnp.lib.capnp._EnumModule, list[str]] = field(default_factory=dict) # Ecus added for data collection, not to be fingerprinted on - extra_ecus: List[Tuple[capnp.lib.capnp._EnumModule, int, Optional[int]]] = field(default_factory=list) + extra_ecus: list[tuple[capnp.lib.capnp._EnumModule, int, int | None]] = field(default_factory=list) # Function a brand can implement to provide better fuzzy matching. Takes in FW versions, # returns set of candidates. Only will match if one candidate is returned - match_fw_to_car_fuzzy: Optional[Callable[[LiveFwVersions, OfflineFwVersions], Set[str]]] = None + match_fw_to_car_fuzzy: Callable[[LiveFwVersions, OfflineFwVersions], set[str]] | None = None def __post_init__(self): for i in range(len(self.requests)): diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 44607d8357..1cf4cecd3e 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from collections import defaultdict -from typing import Any, DefaultDict, Dict, List, Optional, Set +from typing import Any, TypeVar +from collections.abc import Iterator from tqdm import tqdm import capnp @@ -24,20 +25,22 @@ VERSIONS = get_interface_attr('FW_VERSIONS', ignore_none=True) MODEL_TO_BRAND = {c: b for b, e in VERSIONS.items() for c in e} REQUESTS = [(brand, config, r) for brand, config in FW_QUERY_CONFIGS.items() for r in config.requests] +T = TypeVar('T') -def chunks(l, n=128): + +def chunks(l: list[T], n: int = 128) -> Iterator[list[T]]: for i in range(0, len(l), n): yield l[i:i + n] -def is_brand(brand: str, filter_brand: Optional[str]) -> bool: +def is_brand(brand: str, filter_brand: str | None) -> bool: """Returns if brand matches filter_brand or no brand filter is specified""" return filter_brand is None or brand == filter_brand -def build_fw_dict(fw_versions: List[capnp.lib.capnp._DynamicStructBuilder], - filter_brand: Optional[str] = None) -> Dict[AddrType, Set[bytes]]: - fw_versions_dict: DefaultDict[AddrType, Set[bytes]] = defaultdict(set) +def build_fw_dict(fw_versions: list[capnp.lib.capnp._DynamicStructBuilder], + filter_brand: str | None = None) -> dict[AddrType, set[bytes]]: + fw_versions_dict: defaultdict[AddrType, set[bytes]] = defaultdict(set) for fw in fw_versions: if is_brand(fw.brand, filter_brand) and not fw.logging: sub_addr = fw.subAddress if fw.subAddress != 0 else None @@ -95,7 +98,7 @@ def match_fw_to_car_fuzzy(live_fw_versions, match_brand=None, log=True, exclude= return set() -def match_fw_to_car_exact(live_fw_versions, match_brand=None, log=True, extra_fw_versions=None) -> Set[str]: +def match_fw_to_car_exact(live_fw_versions, match_brand=None, log=True, extra_fw_versions=None) -> set[str]: """Do an exact FW match. Returns all cars that match the given FW versions for a list of "essential" ECUs. If an ECU is not considered essential the FW version can be missing to get a fingerprint, but if it's present it @@ -162,11 +165,11 @@ def match_fw_to_car(fw_versions, allow_exact=True, allow_fuzzy=True, log=True): return True, set() -def get_present_ecus(logcan, sendcan, num_pandas=1) -> Set[EcuAddrBusType]: +def get_present_ecus(logcan, sendcan, num_pandas=1) -> set[EcuAddrBusType]: params = Params() # queries are split by OBD multiplexing mode - queries: Dict[bool, List[List[EcuAddrBusType]]] = {True: [], False: []} - parallel_queries: Dict[bool, List[EcuAddrBusType]] = {True: [], False: []} + queries: dict[bool, list[list[EcuAddrBusType]]] = {True: [], False: []} + parallel_queries: dict[bool, list[EcuAddrBusType]] = {True: [], False: []} responses = set() for brand, config, r in REQUESTS: @@ -201,7 +204,7 @@ def get_present_ecus(logcan, sendcan, num_pandas=1) -> Set[EcuAddrBusType]: return ecu_responses -def get_brand_ecu_matches(ecu_rx_addrs: Set[EcuAddrBusType]) -> dict[str, set[AddrType]]: +def get_brand_ecu_matches(ecu_rx_addrs: set[EcuAddrBusType]) -> dict[str, set[AddrType]]: """Returns dictionary of brands and matches with ECUs in their FW versions""" brand_addrs = {brand: {(addr, subaddr) for _, addr, subaddr in config.get_all_ecus(VERSIONS[brand])} for @@ -229,7 +232,7 @@ def set_obd_multiplexing(params: Params, obd_multiplexing: bool): def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, num_pandas=1, debug=False, progress=False) -> \ - List[capnp.lib.capnp._DynamicStructBuilder]: + list[capnp.lib.capnp._DynamicStructBuilder]: """Queries for FW versions ordering brands by likelihood, breaks when exact match is found""" all_car_fw = [] @@ -252,7 +255,7 @@ def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, num_pand def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, num_pandas=1, debug=False, progress=False) -> \ - List[capnp.lib.capnp._DynamicStructBuilder]: + list[capnp.lib.capnp._DynamicStructBuilder]: versions = VERSIONS.copy() params = Params() diff --git a/selfdrive/car/gm/fingerprints.py b/selfdrive/car/gm/fingerprints.py index c349ee2856..73a205a250 100644 --- a/selfdrive/car/gm/fingerprints.py +++ b/selfdrive/car/gm/fingerprints.py @@ -2,6 +2,7 @@ from openpilot.selfdrive.car.gm.values import CAR # Trailblazer also matches as a SILVERADO, TODO: split with fw versions +# FIXME: There are Equinox users with different message lengths, specifically 304 and 320 FINGERPRINTS = { @@ -52,6 +53,9 @@ FINGERPRINTS = { }], CAR.EQUINOX: [{ 190: 6, 193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 257: 8, 288: 5, 289: 8, 298: 8, 304: 1, 309: 8, 311: 8, 313: 8, 320: 3, 328: 1, 352: 5, 381: 8, 384: 4, 386: 8, 388: 8, 413: 8, 451: 8, 452: 8, 453: 6, 455: 7, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 497: 8, 500: 6, 501: 8, 510: 8, 528: 5, 532: 6, 560: 8, 562: 8, 563: 5, 565: 5, 587: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 715: 8, 717: 5, 753: 5, 761: 7, 789: 5, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 869: 4, 880: 6, 977: 8, 1001: 8, 1011: 6, 1017: 8, 1020: 8, 1033: 7, 1034: 7, 1217: 8, 1221: 5, 1233: 8, 1249: 8, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 1, 1271: 8, 1280: 4, 1296: 4, 1300: 8, 1611: 8, 1930: 7 + }, + { + 190: 6, 201: 8, 211: 2, 717: 5, 241: 6, 451: 8, 298: 8, 452: 8, 453: 6, 479: 3, 485: 8, 249: 8, 500: 6, 587: 8, 1611: 8, 289: 8, 481: 7, 193: 8, 197: 8, 209: 7, 455: 7, 489: 8, 309: 8, 413: 8, 501: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 311: 8, 510: 8, 528: 5, 532: 6, 715: 8, 560: 8, 562: 8, 707: 8, 789: 5, 869: 4, 880: 6, 761: 7, 840: 5, 842: 5, 844: 8, 313: 8, 381: 8, 386: 8, 810: 8, 322: 7, 384: 4, 800: 6, 1033: 7, 1034: 7, 1296: 4, 753: 5, 388: 8, 288: 5, 497: 8, 463: 3, 304: 3, 977: 8, 1001: 8, 1280: 4, 320: 4, 352: 5, 563: 5, 565: 5, 1221: 5, 1011: 6, 1017: 8, 1020: 8, 1249: 8, 1300: 8, 328: 1, 1217: 8, 1233: 8, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 1, 1930: 7, 1271: 8 }], } diff --git a/selfdrive/car/gm/interface.py b/selfdrive/car/gm/interface.py index 0c78b0061d..05caa28510 100755 --- a/selfdrive/car/gm/interface.py +++ b/selfdrive/car/gm/interface.py @@ -137,7 +137,7 @@ class CarInterface(CarInterfaceBase): # These cars have been put into dashcam only due to both a lack of users and test coverage. # These cars likely still work fine. Once a user confirms each car works and a test route is # added to selfdrive/car/tests/routes.py, we can remove it from this list. - ret.dashcamOnly = candidate in {CAR.CADILLAC_ATS, CAR.HOLDEN_ASTRA, CAR.MALIBU, CAR.BUICK_REGAL, CAR.EQUINOX} or \ + ret.dashcamOnly = candidate in {CAR.CADILLAC_ATS, CAR.HOLDEN_ASTRA, CAR.MALIBU, CAR.BUICK_REGAL} or \ (ret.networkLocation == NetworkLocation.gateway and ret.radarUnavailable) # Start with a baseline tuning for all GM vehicles. Override tuning as needed in each model section below. diff --git a/selfdrive/car/gm/values.py b/selfdrive/car/gm/values.py index 8188ad4e6e..5c4d572fdb 100644 --- a/selfdrive/car/gm/values.py +++ b/selfdrive/car/gm/values.py @@ -1,7 +1,6 @@ from collections import defaultdict from dataclasses import dataclass from enum import Enum, StrEnum -from typing import Dict, List, Union from cereal import car from openpilot.selfdrive.car import dbc_dict @@ -98,7 +97,7 @@ class GMCarInfo(CarInfo): self.footnotes.append(Footnote.OBD_II) -CAR_INFO: Dict[str, Union[GMCarInfo, List[GMCarInfo]]] = { +CAR_INFO: dict[str, GMCarInfo | list[GMCarInfo]] = { CAR.HOLDEN_ASTRA: GMCarInfo("Holden Astra 2017"), CAR.VOLT: GMCarInfo("Chevrolet Volt 2017-18", min_enable_speed=0, video_link="https://youtu.be/QeMCN_4TFfQ"), CAR.CADILLAC_ATS: GMCarInfo("Cadillac ATS Premium Performance 2018"), @@ -181,7 +180,7 @@ FW_QUERY_CONFIG = FwQueryConfig( extra_ecus=[(Ecu.fwdCamera, 0x24b, None)], ) -DBC: Dict[str, Dict[str, str]] = defaultdict(lambda: dbc_dict('gm_global_a_powertrain_generated', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis')) +DBC: dict[str, dict[str, str]] = defaultdict(lambda: dbc_dict('gm_global_a_powertrain_generated', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis')) EV_CAR = {CAR.VOLT, CAR.BOLT_EUV} diff --git a/selfdrive/car/honda/carstate.py b/selfdrive/car/honda/carstate.py index 03aedb31d2..9025f72397 100644 --- a/selfdrive/car/honda/carstate.py +++ b/selfdrive/car/honda/carstate.py @@ -7,8 +7,8 @@ from opendbc.can.can_define import CANDefine from opendbc.can.parser import CANParser from openpilot.selfdrive.car.honda.hondacan import get_cruise_speed_conversion, get_pt_bus from openpilot.selfdrive.car.honda.values import CAR, DBC, STEER_THRESHOLD, HONDA_BOSCH, \ - HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_ALT_BRAKE_SIGNAL, \ - HONDA_BOSCH_RADARLESS + HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_RADARLESS, \ + HondaFlags from openpilot.selfdrive.car.interfaces import CarStateBase TransmissionType = car.CarParams.TransmissionType @@ -44,7 +44,7 @@ def get_can_messages(CP, gearbox_msg): else: messages.append((gearbox_msg, 100)) - if CP.carFingerprint in HONDA_BOSCH_ALT_BRAKE_SIGNAL: + if CP.flags & HondaFlags.BOSCH_ALT_BRAKE: messages.append(("BRAKE_MODULE", 50)) if CP.carFingerprint in (HONDA_BOSCH | {CAR.CIVIC, CAR.ODYSSEY, CAR.ODYSSEY_CHN}): @@ -217,7 +217,7 @@ class CarState(CarStateBase): else: ret.cruiseState.speed = cp.vl["CRUISE"]["CRUISE_SPEED_PCM"] * CV.KPH_TO_MS - if self.CP.carFingerprint in HONDA_BOSCH_ALT_BRAKE_SIGNAL: + if self.CP.flags & HondaFlags.BOSCH_ALT_BRAKE: ret.brakePressed = cp.vl["BRAKE_MODULE"]["BRAKE_PRESSED"] != 0 else: # brake switch has shown some single time step noise, so only considered when diff --git a/selfdrive/car/honda/fingerprints.py b/selfdrive/car/honda/fingerprints.py index 62843472a3..359ae83b15 100644 --- a/selfdrive/car/honda/fingerprints.py +++ b/selfdrive/car/honda/fingerprints.py @@ -101,10 +101,6 @@ FW_VERSIONS = { b'39990-TVA-X040\x00\x00', b'39990-TVE-H130\x00\x00', ], - (Ecu.unknown, 0x18da3af1, None): [ - b'39390-TVA-A020\x00\x00', - b'39390-TVA-A120\x00\x00', - ], (Ecu.srs, 0x18da53f1, None): [ b'77959-TBX-H230\x00\x00', b'77959-TVA-A460\x00\x00', @@ -299,6 +295,7 @@ FW_VERSIONS = { (Ecu.srs, 0x18da53f1, None): [ b'77959-TBA-A030\x00\x00', b'77959-TBA-A040\x00\x00', + b'77959-TBG-A020\x00\x00', b'77959-TBG-A030\x00\x00', b'77959-TEA-Q820\x00\x00', ], @@ -680,6 +677,7 @@ FW_VERSIONS = { b'36802-TLA-A040\x00\x00', b'36802-TLA-A050\x00\x00', b'36802-TLA-A060\x00\x00', + b'36802-TLA-A070\x00\x00', b'36802-TMC-Q040\x00\x00', b'36802-TMC-Q070\x00\x00', b'36802-TNY-A030\x00\x00', @@ -834,6 +832,7 @@ FW_VERSIONS = { (Ecu.programmedFuelInjection, 0x18da10f1, None): [ b'37805-5MR-3050\x00\x00', b'37805-5MR-3250\x00\x00', + b'37805-5MR-4070\x00\x00', b'37805-5MR-4080\x00\x00', b'37805-5MR-4180\x00\x00', b'37805-5MR-A240\x00\x00', @@ -879,6 +878,7 @@ FW_VERSIONS = { b'28102-5MX-A900\x00\x00', b'28102-5MX-A910\x00\x00', b'28102-5MX-C001\x00\x00', + b'28102-5MX-C910\x00\x00', b'28102-5MX-D001\x00\x00', b'28102-5MX-D710\x00\x00', b'28102-5MX-K610\x00\x00', @@ -916,6 +916,7 @@ FW_VERSIONS = { b'78109-THR-C320\x00\x00', b'78109-THR-C330\x00\x00', b'78109-THR-CE20\x00\x00', + b'78109-THR-CL10\x00\x00', b'78109-THR-DA20\x00\x00', b'78109-THR-DA30\x00\x00', b'78109-THR-DA40\x00\x00', @@ -1206,6 +1207,7 @@ FW_VERSIONS = { b'39990-T6Z-A020\x00\x00', b'39990-T6Z-A030\x00\x00', b'39990-T6Z-A050\x00\x00', + b'39990-T6Z-A110\x00\x00', ], (Ecu.fwdRadar, 0x18dab0f1, None): [ b'36161-T6Z-A020\x00\x00', @@ -1213,6 +1215,7 @@ FW_VERSIONS = { b'36161-T6Z-A420\x00\x00', b'36161-T6Z-A520\x00\x00', b'36161-T6Z-A620\x00\x00', + b'36161-T6Z-A720\x00\x00', b'36161-TJZ-A120\x00\x00', ], (Ecu.gateway, 0x18daeff1, None): [ @@ -1220,6 +1223,7 @@ FW_VERSIONS = { b'38897-T6Z-A110\x00\x00', ], (Ecu.combinationMeter, 0x18da60f1, None): [ + b'78108-T6Z-AF10\x00\x00', b'78109-T6Z-A420\x00\x00', b'78109-T6Z-A510\x00\x00', b'78109-T6Z-A710\x00\x00', @@ -1237,6 +1241,7 @@ FW_VERSIONS = { b'57114-T6Z-A120\x00\x00', b'57114-T6Z-A130\x00\x00', b'57114-T6Z-A520\x00\x00', + b'57114-T6Z-A610\x00\x00', b'57114-TJZ-A520\x00\x00', ], }, diff --git a/selfdrive/car/honda/interface.py b/selfdrive/car/honda/interface.py index 9f228cd8fb..153fa1e635 100755 --- a/selfdrive/car/honda/interface.py +++ b/selfdrive/car/honda/interface.py @@ -3,8 +3,9 @@ from cereal import car from panda import Panda from openpilot.common.conversions import Conversions as CV from openpilot.common.numpy_fast import interp +from openpilot.selfdrive.car.honda.hondacan import get_pt_bus from openpilot.selfdrive.car.honda.values import CarControllerParams, CruiseButtons, HondaFlags, CAR, HONDA_BOSCH, HONDA_NIDEC_ALT_SCM_MESSAGES, \ - HONDA_BOSCH_ALT_BRAKE_SIGNAL, HONDA_BOSCH_RADARLESS + HONDA_BOSCH_RADARLESS from openpilot.selfdrive.car import create_button_events, get_safety_config from openpilot.selfdrive.car.interfaces import CarInterfaceBase from openpilot.selfdrive.car.disable_ecu import disable_ecu @@ -275,7 +276,8 @@ class CarInterface(CarInterfaceBase): raise ValueError(f"unsupported car {candidate}") # These cars use alternate user brake msg (0x1BE) - if candidate in HONDA_BOSCH_ALT_BRAKE_SIGNAL: + if 0x1BE in fingerprint[get_pt_bus(candidate)] and candidate in HONDA_BOSCH: + ret.flags |= HondaFlags.BOSCH_ALT_BRAKE.value ret.safetyConfigs[0].safetyParam |= Panda.FLAG_HONDA_ALT_BRAKE # These cars use alternate SCM messages (SCM_FEEDBACK AND SCM_BUTTON) diff --git a/selfdrive/car/honda/values.py b/selfdrive/car/honda/values.py index 1d4a174b9b..a2ef757d15 100644 --- a/selfdrive/car/honda/values.py +++ b/selfdrive/car/honda/values.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from enum import Enum, IntFlag, StrEnum -from typing import Dict, List, Optional, Union from cereal import car from openpilot.common.conversions import Conversions as CV @@ -49,6 +48,7 @@ class CarControllerParams: class HondaFlags(IntFlag): # Bosch models with alternate set of LKAS_HUD messages BOSCH_EXT_HUD = 1 + BOSCH_ALT_BRAKE = 2 # Car button codes @@ -115,7 +115,7 @@ class HondaCarInfo(CarInfo): self.car_parts = CarParts.common([CarHarness.nidec]) -CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = { +CAR_INFO: dict[str, HondaCarInfo | list[HondaCarInfo] | None] = { CAR.ACCORD: [ HondaCarInfo("Honda Accord 2018-22", "All", video_link="https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS), HondaCarInfo("Honda Inspire 2018", "All", min_steer_speed=3. * CV.MPH_TO_MS), @@ -149,7 +149,7 @@ CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = { HondaCarInfo("Honda Pilot 2016-22", min_steer_speed=12. * CV.MPH_TO_MS), HondaCarInfo("Honda Passport 2019-23", "All", min_steer_speed=12. * CV.MPH_TO_MS), ], - CAR.RIDGELINE: HondaCarInfo("Honda Ridgeline 2017-23", min_steer_speed=12. * CV.MPH_TO_MS), + CAR.RIDGELINE: HondaCarInfo("Honda Ridgeline 2017-24", min_steer_speed=12. * CV.MPH_TO_MS), CAR.INSIGHT: HondaCarInfo("Honda Insight 2019-22", "All", min_steer_speed=3. * CV.MPH_TO_MS), CAR.HONDA_E: HondaCarInfo("Honda e 2020", "All", min_steer_speed=3. * CV.MPH_TO_MS), } @@ -195,10 +195,19 @@ FW_QUERY_CONFIG = FwQueryConfig( [StdQueries.UDS_VERSION_REQUEST], [StdQueries.UDS_VERSION_RESPONSE], bus=1, - logging=True, obd_multiplexing=False, ), ], + # We lose these ECUs without the comma power on these cars. + # Note that we still attempt to match with them when they are present + non_essential_ecus={ + Ecu.programmedFuelInjection: [CAR.CIVIC_BOSCH, CAR.CRV_5G], + Ecu.transmission: [CAR.CIVIC_BOSCH, CAR.CRV_5G], + Ecu.vsa: [CAR.CIVIC_BOSCH, CAR.CRV_5G], + Ecu.combinationMeter: [CAR.CIVIC_BOSCH, CAR.CRV_5G], + Ecu.gateway: [CAR.CIVIC_BOSCH, CAR.CRV_5G], + Ecu.electricBrakeBooster: [CAR.CIVIC_BOSCH, CAR.CRV_5G], + }, extra_ecus=[ # The only other ECU on PT bus accessible by camera on radarless Civic (Ecu.unknown, 0x18DAB3F1, None), @@ -243,5 +252,4 @@ HONDA_NIDEC_ALT_SCM_MESSAGES = {CAR.ACURA_ILX, CAR.ACURA_RDX, CAR.CRV, CAR.CRV_E CAR.PILOT, CAR.RIDGELINE} HONDA_BOSCH = {CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_5G, CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E, CAR.CIVIC_2022, CAR.HRV_3G} -HONDA_BOSCH_ALT_BRAKE_SIGNAL = {CAR.ACCORD, CAR.CRV_5G, CAR.ACURA_RDX_3G, CAR.HRV_3G} HONDA_BOSCH_RADARLESS = {CAR.CIVIC_2022, CAR.HRV_3G} diff --git a/selfdrive/car/hyundai/fingerprints.py b/selfdrive/car/hyundai/fingerprints.py index 5f42ad4918..d1fc1faabb 100644 --- a/selfdrive/car/hyundai/fingerprints.py +++ b/selfdrive/car/hyundai/fingerprints.py @@ -973,6 +973,7 @@ FW_VERSIONS = { b'\xf1\x00BD MDPS C 1.00 1.02 56310-XX000 4BD2C102', b'\xf1\x00BD MDPS C 1.00 1.08 56310/M6300 4BDDC108', b'\xf1\x00BD MDPS C 1.00 1.08 56310M6300\x00 4BDDC108', + b'\xf1\x00BDm MDPS C A.01 1.01 56310M7800\x00 4BPMC101', b'\xf1\x00BDm MDPS C A.01 1.03 56310M7800\x00 4BPMC103', ], (Ecu.fwdCamera, 0x7c4, None): [ @@ -989,11 +990,13 @@ FW_VERSIONS = { b'\xf1\x81616F2051\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.abs, 0x7d1, None): [ + b'\xf1\x00\x00\x00\x00\x00\x00\x00', b'\xf1\x816VGRAH00018.ELF\xf1\x00\x00\x00\x00\x00\x00\x00', b'\xf1\x8758900-M7AB0 \xf1\x816VQRAD00127.ELF\xf1\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x006V2B0_C2\x00\x006V2C6051\x00\x00CBD0N20NL1\x00\x00\x00\x00', + b'\xf1\x006V2B0_C2\x00\x006V2C6051\x00\x00CBD0N20NL1\x90@\xc6\xae', b'\xf1\x816U2VC051\x00\x00\xf1\x006U2V0_C2\x00\x006U2VC051\x00\x00DBD0T16SS0\x00\x00\x00\x00', b"\xf1\x816U2VC051\x00\x00\xf1\x006U2V0_C2\x00\x006U2VC051\x00\x00DBD0T16SS0\xcf\x1e'\xc3", ], @@ -1546,6 +1549,7 @@ FW_VERSIONS = { b'\xf1\x00NE1 MFC AT EUR LHD 1.00 1.06 99211-GI000 210813', b'\xf1\x00NE1 MFC AT EUR RHD 1.00 1.01 99211-GI010 211007', b'\xf1\x00NE1 MFC AT EUR RHD 1.00 1.02 99211-GI010 211206', + b'\xf1\x00NE1 MFC AT KOR LHD 1.00 1.00 99211-GI020 230719', b'\xf1\x00NE1 MFC AT KOR LHD 1.00 1.05 99211-GI010 220614', b'\xf1\x00NE1 MFC AT USA LHD 1.00 1.00 99211-GI020 230719', b'\xf1\x00NE1 MFC AT USA LHD 1.00 1.01 99211-GI010 211007', @@ -1566,6 +1570,7 @@ FW_VERSIONS = { }, CAR.TUCSON_4TH_GEN: { (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00NX4 FR_CMR AT EUR LHD 1.00 1.00 99211-N9220 14K', b'\xf1\x00NX4 FR_CMR AT EUR LHD 1.00 2.02 99211-N9000 14E', b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-N9210 14G', b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-N9220 14K', @@ -1578,6 +1583,7 @@ FW_VERSIONS = { (Ecu.fwdRadar, 0x7d0, None): [ b'\xf1\x00NX4__ 1.00 1.00 99110-N9100 ', b'\xf1\x00NX4__ 1.00 1.01 99110-N9000 ', + b'\xf1\x00NX4__ 1.00 1.02 99110-N9000 ', b'\xf1\x00NX4__ 1.01 1.00 99110-N9100 ', ], }, @@ -1636,6 +1642,7 @@ FW_VERSIONS = { ], (Ecu.fwdRadar, 0x7d0, None): [ b'\xf1\x00MQ4_ SCC F-CUP 1.00 1.06 99110-P2000 ', + b'\xf1\x00MQ4_ SCC FHCUP 1.00 1.00 99110-R5000 ', b'\xf1\x00MQ4_ SCC FHCUP 1.00 1.06 99110-P2000 ', b'\xf1\x00MQ4_ SCC FHCUP 1.00 1.08 99110-P2000 ', ], diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index a3210f8071..049a63399c 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -11,7 +11,6 @@ from openpilot.selfdrive.car.interfaces import CarInterfaceBase from openpilot.selfdrive.car.disable_ecu import disable_ecu Ecu = car.CarParams.Ecu -SafetyModel = car.CarParams.SafetyModel ButtonType = car.CarState.ButtonEvent.Type EventName = car.CarEvent.EventName ENABLE_BUTTONS = (Buttons.RES_ACCEL, Buttons.SET_DECEL, Buttons.CANCEL) @@ -19,17 +18,6 @@ BUTTONS_DICT = {Buttons.RES_ACCEL: ButtonType.accelCruise, Buttons.SET_DECEL: Bu Buttons.GAP_DIST: ButtonType.gapAdjustCruise, Buttons.CANCEL: ButtonType.cancel} -def set_safety_config_hyundai(candidate, CAN, can_fd=False): - platform = SafetyModel.hyundaiCanfd if can_fd else \ - SafetyModel.hyundaiLegacy if candidate in LEGACY_SAFETY_MODE_CAR else \ - SafetyModel.hyundai - cfgs = [get_safety_config(platform), ] - if CAN.ECAN >= 4: - cfgs.insert(0, get_safety_config(SafetyModel.noOutput)) - - return cfgs - - class CarInterface(CarInterfaceBase): @staticmethod def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): @@ -54,6 +42,7 @@ class CarInterface(CarInterfaceBase): # detect HDA2 with ADAS Driving ECU if hda2: + ret.flags |= HyundaiFlags.CANFD_HDA2.value if 0x110 in fingerprint[CAN.CAM]: ret.flags |= HyundaiFlags.CANFD_HDA2_ALT_STEERING.value else: @@ -303,20 +292,30 @@ class CarInterface(CarInterfaceBase): ret.enableBsm = 0x58b in fingerprint[0] # *** panda safety config *** - ret.safetyConfigs = set_safety_config_hyundai(candidate, CAN, can_fd=(candidate in CANFD_CAR)) - - if hda2: - ret.flags |= HyundaiFlags.CANFD_HDA2.value - ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_HDA2 - if candidate in CANFD_CAR: - if hda2 and ret.flags & HyundaiFlags.CANFD_HDA2_ALT_STEERING: - ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_HDA2_ALT_STEERING + cfgs = [get_safety_config(car.CarParams.SafetyModel.hyundaiCanfd), ] + if CAN.ECAN >= 4: + cfgs.insert(0, get_safety_config(car.CarParams.SafetyModel.noOutput)) + ret.safetyConfigs = cfgs + + if ret.flags & HyundaiFlags.CANFD_HDA2: + ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_HDA2 + if ret.flags & HyundaiFlags.CANFD_HDA2_ALT_STEERING: + ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_HDA2_ALT_STEERING if ret.flags & HyundaiFlags.CANFD_ALT_BUTTONS: ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_ALT_BUTTONS + if ret.flags & HyundaiFlags.CANFD_CAMERA_SCC: + ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CAMERA_SCC + else: + if candidate in LEGACY_SAFETY_MODE_CAR: + # these cars require a special panda safety mode due to missing counters and checksums in the messages + ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.hyundaiLegacy)] + else: + ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.hyundai, 0)] + + if candidate in CAMERA_SCC_CAR: + ret.safetyConfigs[0].safetyParam |= Panda.FLAG_HYUNDAI_CAMERA_SCC - if ret.flags & HyundaiFlags.CANFD_CAMERA_SCC or candidate in CAMERA_SCC_CAR: - ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CAMERA_SCC if ret.openpilotLongitudinalControl: ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_LONG if ret.flags & HyundaiFlags.HYBRID: diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index f7986fc202..76ff0b39ce 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -1,7 +1,6 @@ import re from dataclasses import dataclass from enum import Enum, IntFlag, StrEnum -from typing import Dict, List, Optional, Set, Tuple, Union from cereal import car from panda.python import uds @@ -157,7 +156,7 @@ class HyundaiCarInfo(CarInfo): self.footnotes.insert(0, Footnote.CANFD) -CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = { +CAR_INFO: dict[str, HyundaiCarInfo | list[HyundaiCarInfo] | None] = { CAR.AZERA_6TH_GEN: HyundaiCarInfo("Hyundai Azera 2022", "All", car_parts=CarParts.common([CarHarness.hyundai_k])), CAR.AZERA_HEV_6TH_GEN: [ HyundaiCarInfo("Hyundai Azera Hybrid 2019", "All", car_parts=CarParts.common([CarHarness.hyundai_c])), @@ -316,7 +315,7 @@ class Buttons: CANCEL = 4 # on newer models, this is a pause/resume button -def get_platform_codes(fw_versions: List[bytes]) -> Set[Tuple[bytes, Optional[bytes]]]: +def get_platform_codes(fw_versions: list[bytes]) -> set[tuple[bytes, bytes | None]]: # Returns unique, platform-specific identification codes for a set of versions codes = set() # (code-Optional[part], date) for fw in fw_versions: @@ -335,12 +334,12 @@ def get_platform_codes(fw_versions: List[bytes]) -> Set[Tuple[bytes, Optional[by return codes -def match_fw_to_car_fuzzy(live_fw_versions, offline_fw_versions) -> Set[str]: +def match_fw_to_car_fuzzy(live_fw_versions, offline_fw_versions) -> set[str]: # Non-electric CAN FD platforms often do not have platform code specifiers needed # to distinguish between hybrid and ICE. All EVs so far are either exclusively # electric or specify electric in the platform code. fuzzy_platform_blacklist = {str(c) for c in (CANFD_CAR - EV_CAR - CANFD_FUZZY_WHITELIST)} - candidates: Set[str] = set() + candidates: set[str] = set() for candidate, fws in offline_fw_versions.items(): # Keep track of ECUs which pass all checks (platform codes, within date range) @@ -396,6 +395,9 @@ HYUNDAI_VERSION_REQUEST_MULTI = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER] p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION) + \ p16(0xf100) +HYUNDAI_ECU_MANUFACTURING_DATE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \ + p16(uds.DATA_IDENTIFIER_TYPE.ECU_MANUFACTURING_DATE) + HYUNDAI_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) # Regex patterns for parsing platform code, FW date, and part number from FW versions @@ -414,6 +416,8 @@ PLATFORM_CODE_ECUS = [Ecu.fwdRadar, Ecu.fwdCamera, Ecu.eps] # TODO: there are date codes in the ABS firmware versions in hex DATE_FW_ECUS = [Ecu.fwdCamera] +ALL_HYUNDAI_ECUS = [Ecu.eps, Ecu.abs, Ecu.fwdRadar, Ecu.fwdCamera, Ecu.engine, Ecu.parkingAdas, Ecu.transmission, Ecu.adas, Ecu.hvac, Ecu.cornerRadar] + FW_QUERY_CONFIG = FwQueryConfig( requests=[ # TODO: minimize shared whitelists for CAN and cornerRadar for CAN-FD @@ -447,13 +451,52 @@ FW_QUERY_CONFIG = FwQueryConfig( obd_multiplexing=False, ), - # CAN-FD debugging queries + # CAN & CAN FD query to understand the three digit date code + # HDA2 cars usually use 6 digit date codes, so skip bus 1 + Request( + [HYUNDAI_ECU_MANUFACTURING_DATE], + [HYUNDAI_VERSION_RESPONSE], + whitelist_ecus=[Ecu.fwdCamera], + bus=0, + auxiliary=True, + logging=True, + ), + + # CAN & CAN FD logging queries (from camera) + Request( + [HYUNDAI_VERSION_REQUEST_LONG], + [HYUNDAI_VERSION_RESPONSE], + whitelist_ecus=ALL_HYUNDAI_ECUS, + bus=0, + auxiliary=True, + logging=True, + ), + Request( + [HYUNDAI_VERSION_REQUEST_MULTI], + [HYUNDAI_VERSION_RESPONSE], + whitelist_ecus=ALL_HYUNDAI_ECUS, + bus=0, + auxiliary=True, + logging=True, + ), + Request( + [HYUNDAI_VERSION_REQUEST_LONG], + [HYUNDAI_VERSION_RESPONSE], + whitelist_ecus=ALL_HYUNDAI_ECUS, + bus=1, + auxiliary=True, + obd_multiplexing=False, + logging=True, + ), + + # CAN-FD alt request logging queries Request( [HYUNDAI_VERSION_REQUEST_ALT], [HYUNDAI_VERSION_RESPONSE], whitelist_ecus=[Ecu.parkingAdas, Ecu.hvac], bus=0, auxiliary=True, + logging=True, ), Request( [HYUNDAI_VERSION_REQUEST_ALT], @@ -461,6 +504,7 @@ FW_QUERY_CONFIG = FwQueryConfig( whitelist_ecus=[Ecu.parkingAdas, Ecu.hvac], bus=1, auxiliary=True, + logging=True, obd_multiplexing=False, ), ], diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index b7357d2c93..05610a6dd6 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -1,11 +1,11 @@ import json import os -import time import numpy as np import tomllib from abc import abstractmethod, ABC from enum import StrEnum -from typing import Any, Dict, Optional, Tuple, List, Callable, NamedTuple +from typing import Any, NamedTuple, cast +from collections.abc import Callable from cereal import car from openpilot.common.basedir import BASEDIR @@ -13,7 +13,7 @@ from openpilot.common.conversions import Conversions as CV from openpilot.common.simple_kalman import KF1D, get_kalman_gain from openpilot.common.numpy_fast import clip from openpilot.common.realtime import DT_CTRL -from openpilot.selfdrive.car import apply_hysteresis, gen_empty_fingerprint, scale_rot_inertia, scale_tire_stiffness, STD_CARGO_KG +from openpilot.selfdrive.car import PlatformConfig, apply_hysteresis, gen_empty_fingerprint, scale_rot_inertia, scale_tire_stiffness, STD_CARGO_KG from openpilot.selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX, get_friction from openpilot.selfdrive.controls.lib.events import Events from openpilot.selfdrive.controls.lib.vehicle_model import VehicleModel @@ -108,8 +108,16 @@ class CarInterfaceBase(ABC): return cls.get_params(candidate, gen_empty_fingerprint(), list(), False, False) @classmethod - def get_params(cls, candidate: str, fingerprint: Dict[int, Dict[int, int]], car_fw: List[car.CarParams.CarFw], experimental_long: bool, docs: bool): + def get_params(cls, candidate: str, fingerprint: dict[int, dict[int, int]], car_fw: list[car.CarParams.CarFw], experimental_long: bool, docs: bool): ret = CarInterfaceBase.get_std_params(candidate) + + if hasattr(candidate, "config"): + platform_config = cast(PlatformConfig, candidate.config) + if platform_config.specs is not None: + ret.mass = platform_config.specs.mass + ret.wheelbase = platform_config.specs.wheelbase + ret.steerRatio = platform_config.specs.steerRatio + ret = cls._get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs) # Vehicle mass is published curb weight plus assumed payload such as a human driver; notCars have no assumed payload @@ -124,8 +132,8 @@ class CarInterfaceBase(ABC): @staticmethod @abstractmethod - def _get_params(ret: car.CarParams, candidate: str, fingerprint: Dict[int, Dict[int, int]], - car_fw: List[car.CarParams.CarFw], experimental_long: bool, docs: bool): + def _get_params(ret: car.CarParams, candidate: str, fingerprint: dict[int, dict[int, int]], + car_fw: list[car.CarParams.CarFw], experimental_long: bool, docs: bool): raise NotImplementedError @staticmethod @@ -205,7 +213,7 @@ class CarInterfaceBase(ABC): def _update(self, c: car.CarControl) -> car.CarState: pass - def update(self, c: car.CarControl, can_strings: List[bytes]) -> car.CarState: + def update(self, c: car.CarControl, can_strings: list[bytes]) -> car.CarState: # parse can for cp in self.can_parsers: if cp is not None: @@ -239,7 +247,7 @@ class CarInterfaceBase(ABC): return reader @abstractmethod - def apply(self, c: car.CarControl, now_nanos: int) -> Tuple[car.CarControl.Actuators, List[bytes]]: + def apply(self, c: car.CarControl, now_nanos: int) -> tuple[car.CarControl.Actuators, list[bytes]]: pass def create_common_events(self, cs_out, extra_gears=None, pcm_enable=True, allow_enable=True, @@ -322,13 +330,14 @@ class RadarInterfaceBase(ABC): self.pts = {} self.delay = 0 self.radar_ts = CP.radarTimeStep + self.frame = 0 self.no_radar_sleep = 'NO_RADAR_SLEEP' in os.environ def update(self, can_strings): - ret = car.RadarData.new_message() - if not self.no_radar_sleep: - time.sleep(self.radar_ts) # radard runs on RI updates - return ret + self.frame += 1 + if (self.frame % int(100 * self.radar_ts)) == 0: + return car.RadarData.new_message() + return None class CarStateBase(ABC): @@ -409,11 +418,11 @@ class CarStateBase(ABC): return bool(left_blinker_stalk or self.left_blinker_cnt > 0), bool(right_blinker_stalk or self.right_blinker_cnt > 0) @staticmethod - def parse_gear_shifter(gear: Optional[str]) -> car.CarState.GearShifter: + def parse_gear_shifter(gear: str | None) -> car.CarState.GearShifter: if gear is None: return GearShifter.unknown - d: Dict[str, car.CarState.GearShifter] = { + d: dict[str, car.CarState.GearShifter] = { 'P': GearShifter.park, 'PARK': GearShifter.park, 'R': GearShifter.reverse, 'REVERSE': GearShifter.reverse, 'N': GearShifter.neutral, 'NEUTRAL': GearShifter.neutral, @@ -450,7 +459,7 @@ INTERFACE_ATTR_FILE = { # interface-specific helpers -def get_interface_attr(attr: str, combine_brands: bool = False, ignore_none: bool = False) -> Dict[str | StrEnum, Any]: +def get_interface_attr(attr: str, combine_brands: bool = False, ignore_none: bool = False) -> dict[str | StrEnum, Any]: # read all the folders in selfdrive/car and return a dict where: # - keys are all the car models or brand names # - values are attr values from all car folders @@ -483,7 +492,7 @@ class NanoFFModel: self.load_weights(platform) def load_weights(self, platform: str): - with open(self.weights_loc, 'r') as fob: + with open(self.weights_loc) as fob: self.weights = {k: np.array(v) for k, v in json.load(fob)[platform].items()} def relu(self, x: np.ndarray): @@ -498,7 +507,7 @@ class NanoFFModel: x = np.dot(x, self.weights['w_4']) + self.weights['b_4'] return x - def predict(self, x: List[float], do_sample: bool = False): + def predict(self, x: list[float], do_sample: bool = False): x = self.forward(np.array(x)) if do_sample: pred = np.random.laplace(x[0], np.exp(x[1]) / self.weights['temperature']) diff --git a/selfdrive/car/isotp_parallel_query.py b/selfdrive/car/isotp_parallel_query.py index 696935fd35..678fe9ea46 100644 --- a/selfdrive/car/isotp_parallel_query.py +++ b/selfdrive/car/isotp_parallel_query.py @@ -5,11 +5,14 @@ from functools import partial import cereal.messaging as messaging from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.boardd.boardd import can_list_to_can_capnp +from openpilot.selfdrive.car.fw_query_definitions import AddrType from panda.python.uds import CanClient, IsoTpMessage, FUNCTIONAL_ADDRS, get_rx_addr_for_tx_addr class IsoTpParallelQuery: - def __init__(self, sendcan, logcan, bus, addrs, request, response, response_offset=0x8, functional_addrs=None, debug=False, response_pending_timeout=10): + def __init__(self, sendcan: messaging.PubSocket, logcan: messaging.SubSocket, bus: int, addrs: list[int] | list[AddrType], + request: list[bytes], response: list[bytes], response_offset: int = 0x8, + functional_addrs: list[int] | None = None, debug: bool = False, response_pending_timeout: float = 10) -> None: self.sendcan = sendcan self.logcan = logcan self.bus = bus @@ -24,7 +27,7 @@ class IsoTpParallelQuery: assert tx_addr not in FUNCTIONAL_ADDRS, f"Functional address should be defined in functional_addrs: {hex(tx_addr)}" self.msg_addrs = {tx_addr: get_rx_addr_for_tx_addr(tx_addr[0], rx_offset=response_offset) for tx_addr in real_addrs} - self.msg_buffer = defaultdict(list) + self.msg_buffer: dict[int, list[tuple[int, int, bytes, int]]] = defaultdict(list) def rx(self): """Drain can socket and sort messages into buffers based on address""" @@ -63,7 +66,7 @@ class IsoTpParallelQuery: messaging.drain_sock_raw(self.logcan) self.msg_buffer = defaultdict(list) - def _create_isotp_msg(self, tx_addr, sub_addr, rx_addr): + def _create_isotp_msg(self, tx_addr: int, sub_addr: int | None, rx_addr: int): can_client = CanClient(self._can_tx, partial(self._can_rx, rx_addr, sub_addr=sub_addr), tx_addr, rx_addr, self.bus, sub_addr=sub_addr, debug=self.debug) @@ -73,7 +76,7 @@ class IsoTpParallelQuery: # as well as reduces chances we process messages from previous queries return IsoTpMessage(can_client, timeout=0, separation_time=0.01, debug=self.debug, max_len=max_len) - def get_data(self, timeout, total_timeout=60.): + def get_data(self, timeout: float, total_timeout: float = 60.) -> dict[AddrType, bytes]: self._drain_rx() # Create message objects diff --git a/selfdrive/car/mazda/carstate.py b/selfdrive/car/mazda/carstate.py index 1f7846ca06..c0819592d4 100644 --- a/selfdrive/car/mazda/carstate.py +++ b/selfdrive/car/mazda/carstate.py @@ -32,7 +32,7 @@ class CarState(CarStateBase): # Match panda speed reading speed_kph = cp.vl["ENGINE_DATA"]["SPEED"] - ret.standstill = speed_kph < .1 + ret.standstill = speed_kph <= .1 can_gear = int(cp.vl["GEAR"]["GEAR"]) ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(can_gear, None)) diff --git a/selfdrive/car/mazda/values.py b/selfdrive/car/mazda/values.py index b43ab3df66..eaf76d6a72 100644 --- a/selfdrive/car/mazda/values.py +++ b/selfdrive/car/mazda/values.py @@ -1,6 +1,5 @@ from dataclasses import dataclass, field from enum import StrEnum -from typing import Dict, List, Union from cereal import car from openpilot.selfdrive.car import dbc_dict @@ -41,7 +40,7 @@ class MazdaCarInfo(CarInfo): car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.mazda])) -CAR_INFO: Dict[str, Union[MazdaCarInfo, List[MazdaCarInfo]]] = { +CAR_INFO: dict[str, MazdaCarInfo | list[MazdaCarInfo]] = { CAR.CX5: MazdaCarInfo("Mazda CX-5 2017-21"), CAR.CX9: MazdaCarInfo("Mazda CX-9 2016-20"), CAR.MAZDA3: MazdaCarInfo("Mazda 3 2017-18"), diff --git a/selfdrive/car/mock/interface.py b/selfdrive/car/mock/interface.py index c7821bdb97..2e4ac43033 100755 --- a/selfdrive/car/mock/interface.py +++ b/selfdrive/car/mock/interface.py @@ -22,7 +22,7 @@ class CarInterface(CarInterfaceBase): def _update(self, c): self.sm.update(0) - gps_sock = 'gpsLocationExternal' if self.sm.rcv_frame['gpsLocationExternal'] > 1 else 'gpsLocation' + gps_sock = 'gpsLocationExternal' if self.sm.recv_frame['gpsLocationExternal'] > 1 else 'gpsLocation' ret = car.CarState.new_message() ret.vEgo = self.sm[gps_sock].speed diff --git a/selfdrive/car/mock/values.py b/selfdrive/car/mock/values.py index c6c96579b4..e75665c98f 100644 --- a/selfdrive/car/mock/values.py +++ b/selfdrive/car/mock/values.py @@ -1,5 +1,4 @@ from enum import StrEnum -from typing import Dict, List, Optional, Union from openpilot.selfdrive.car.docs_definitions import CarInfo @@ -8,6 +7,6 @@ class CAR(StrEnum): MOCK = 'mock' -CAR_INFO: Dict[str, Optional[Union[CarInfo, List[CarInfo]]]] = { +CAR_INFO: dict[str, CarInfo | list[CarInfo] | None] = { CAR.MOCK: None, } diff --git a/selfdrive/car/nissan/fingerprints.py b/selfdrive/car/nissan/fingerprints.py index 69e88be898..19267ded46 100644 --- a/selfdrive/car/nissan/fingerprints.py +++ b/selfdrive/car/nissan/fingerprints.py @@ -45,15 +45,30 @@ FW_VERSIONS = { }, CAR.LEAF: { (Ecu.abs, 0x740, None): [ + b'476605SA1C', + b'476605SA7D', + b'476605SC2D', + b'476606WK7B', b'476606WK9B', ], (Ecu.eps, 0x742, None): [ + b'5SA2A\x99A\x05\x02N123F\x15b\x00\x00\x00\x00\x00\x00\x00\x80', + b'5SA2A\xb7A\x05\x02N123F\x15\xa2\x00\x00\x00\x00\x00\x00\x00\x80', + b'5SN2A\xb7A\x05\x02N123F\x15\xa2\x00\x00\x00\x00\x00\x00\x00\x80', b'5SN2A\xb7A\x05\x02N126F\x15\xb2\x00\x00\x00\x00\x00\x00\x00\x80', ], (Ecu.fwdCamera, 0x707, None): [ + b'5SA0ADB\x04\x18\x00\x00\x00\x00\x00_*6\x04\x94a\x00\x00\x00\x80', + b'5SA2ADB\x04\x18\x00\x00\x00\x00\x00_*6\x04\x94a\x00\x00\x00\x80', + b'6WK2ADB\x04\x18\x00\x00\x00\x00\x00R;1\x18\x99\x10\x00\x00\x00\x80', + b'6WK2BDB\x04\x18\x00\x00\x00\x00\x00R;1\x18\x99\x10\x00\x00\x00\x80', b'6WK2CDB\x04\x18\x00\x00\x00\x00\x00R=1\x18\x99\x10\x00\x00\x00\x80', ], (Ecu.gateway, 0x18dad0f1, None): [ + b'284U25SA3C', + b'284U25SP0C', + b'284U25SP1C', + b'284U26WK0A', b'284U26WK0C', ], }, diff --git a/selfdrive/car/nissan/values.py b/selfdrive/car/nissan/values.py index d064ce894d..c0308f9e0d 100644 --- a/selfdrive/car/nissan/values.py +++ b/selfdrive/car/nissan/values.py @@ -1,6 +1,5 @@ from dataclasses import dataclass, field from enum import StrEnum -from typing import Dict, List, Optional, Union from cereal import car from panda.python import uds @@ -37,7 +36,7 @@ class NissanCarInfo(CarInfo): car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.nissan_a])) -CAR_INFO: Dict[str, Optional[Union[NissanCarInfo, List[NissanCarInfo]]]] = { +CAR_INFO: dict[str, NissanCarInfo | list[NissanCarInfo] | None] = { CAR.XTRAIL: NissanCarInfo("Nissan X-Trail 2017"), CAR.LEAF: NissanCarInfo("Nissan Leaf 2018-23", video_link="https://youtu.be/vaMbtAh_0cY"), CAR.LEAF_IC: None, # same platforms diff --git a/selfdrive/car/subaru/fingerprints.py b/selfdrive/car/subaru/fingerprints.py index ad8ebe87cd..90fa6093d9 100644 --- a/selfdrive/car/subaru/fingerprints.py +++ b/selfdrive/car/subaru/fingerprints.py @@ -509,17 +509,20 @@ FW_VERSIONS = { (Ecu.fwdCamera, 0x787, None): [ b'\x04!\x01\x1eD\x07!\x00\x04,', b'\x04!\x08\x01.\x07!\x08\x022', + b'\r!\x08\x017\n!\x08\x003', ], (Ecu.engine, 0x7e0, None): [ b'\xd5"`0\x07', b'\xd5"a0\x07', b'\xf1"`q\x07', b'\xf1"aq\x07', + b'\xfa"ap\x07', ], (Ecu.transmission, 0x7e1, None): [ b'\x1d\x86B0\x00', b'\x1d\xf6B0\x00', b'\x1e\x86B0\x00', + b'\x1e\x86F0\x00', b'\x1e\xf6D0\x00', ], }, diff --git a/selfdrive/car/subaru/interface.py b/selfdrive/car/subaru/interface.py index 1296aead5e..edf07ac2ef 100644 --- a/selfdrive/car/subaru/interface.py +++ b/selfdrive/car/subaru/interface.py @@ -40,11 +40,10 @@ class CarInterface(CarInterfaceBase): else: CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) + + ret.centerToFront = ret.wheelbase * 0.5 + if candidate in (CAR.ASCENT, CAR.ASCENT_2023): - ret.mass = 2031. - ret.wheelbase = 2.89 - ret.centerToFront = ret.wheelbase * 0.5 - ret.steerRatio = 13.5 ret.steerActuatorDelay = 0.3 # end-to-end angle controller ret.lateralTuning.init('pid') ret.lateralTuning.pid.kf = 0.00003 @@ -52,10 +51,6 @@ class CarInterface(CarInterfaceBase): ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.0025, 0.1], [0.00025, 0.01]] elif candidate == CAR.IMPREZA: - ret.mass = 1568. - ret.wheelbase = 2.67 - ret.centerToFront = ret.wheelbase * 0.5 - ret.steerRatio = 15 ret.steerActuatorDelay = 0.4 # end-to-end angle controller ret.lateralTuning.init('pid') ret.lateralTuning.pid.kf = 0.00005 @@ -63,58 +58,31 @@ class CarInterface(CarInterfaceBase): ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2, 0.3], [0.02, 0.03]] elif candidate == CAR.IMPREZA_2020: - ret.mass = 1480. - ret.wheelbase = 2.67 - ret.centerToFront = ret.wheelbase * 0.5 - ret.steerRatio = 17 # learned, 14 stock ret.lateralTuning.init('pid') ret.lateralTuning.pid.kf = 0.00005 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 14., 23.], [0., 14., 23.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.045, 0.042, 0.20], [0.04, 0.035, 0.045]] elif candidate == CAR.CROSSTREK_HYBRID: - ret.mass = 1668. - ret.wheelbase = 2.67 - ret.centerToFront = ret.wheelbase * 0.5 - ret.steerRatio = 17 ret.steerActuatorDelay = 0.1 elif candidate in (CAR.FORESTER, CAR.FORESTER_2022, CAR.FORESTER_HYBRID): - ret.mass = 1568. - ret.wheelbase = 2.67 - ret.centerToFront = ret.wheelbase * 0.5 - ret.steerRatio = 17 # learned, 14 stock ret.lateralTuning.init('pid') ret.lateralTuning.pid.kf = 0.000038 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 14., 23.], [0., 14., 23.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.01, 0.065, 0.2], [0.001, 0.015, 0.025]] elif candidate in (CAR.OUTBACK, CAR.LEGACY, CAR.OUTBACK_2023): - ret.mass = 1568. - ret.wheelbase = 2.67 - ret.centerToFront = ret.wheelbase * 0.5 - ret.steerRatio = 17 ret.steerActuatorDelay = 0.1 elif candidate in (CAR.FORESTER_PREGLOBAL, CAR.OUTBACK_PREGLOBAL_2018): ret.safetyConfigs[0].safetyParam = Panda.FLAG_SUBARU_PREGLOBAL_REVERSED_DRIVER_TORQUE # Outback 2018-2019 and Forester have reversed driver torque signal - ret.mass = 1568 - ret.wheelbase = 2.67 - ret.centerToFront = ret.wheelbase * 0.5 - ret.steerRatio = 20 # learned, 14 stock elif candidate == CAR.LEGACY_PREGLOBAL: - ret.mass = 1568 - ret.wheelbase = 2.67 - ret.centerToFront = ret.wheelbase * 0.5 - ret.steerRatio = 12.5 # 14.5 stock ret.steerActuatorDelay = 0.15 elif candidate == CAR.OUTBACK_PREGLOBAL: - ret.mass = 1568 - ret.wheelbase = 2.67 - ret.centerToFront = ret.wheelbase * 0.5 - ret.steerRatio = 20 # learned, 14 stock + pass else: raise ValueError(f"unknown car: {candidate}") diff --git a/selfdrive/car/subaru/subarucan.py b/selfdrive/car/subaru/subarucan.py index 290737c3e6..86d39ff885 100644 --- a/selfdrive/car/subaru/subarucan.py +++ b/selfdrive/car/subaru/subarucan.py @@ -13,6 +13,15 @@ def create_steering_control(packer, apply_steer, steer_req): return packer.make_can_msg("ES_LKAS", 0, values) +def create_steering_control_angle(packer, apply_steer, steer_req): + values = { + "LKAS_Output": apply_steer, + "LKAS_Request": steer_req, + "SET_3": 3 + } + return packer.make_can_msg("ES_LKAS_ANGLE", 0, values) + + def create_steering_status(packer): return packer.make_can_msg("ES_LKAS_State", 0, {}) diff --git a/selfdrive/car/subaru/values.py b/selfdrive/car/subaru/values.py index d9d2f78cea..e496f43628 100644 --- a/selfdrive/car/subaru/values.py +++ b/selfdrive/car/subaru/values.py @@ -1,10 +1,9 @@ from dataclasses import dataclass, field -from enum import Enum, IntFlag, StrEnum -from typing import Dict, List, Union +from enum import Enum, IntFlag from cereal import car from panda.python import uds -from openpilot.selfdrive.car import dbc_dict +from openpilot.selfdrive.car import CarSpecs, DbcDict, PlatformConfig, Platforms, dbc_dict from openpilot.selfdrive.car.docs_definitions import CarFootnote, CarHarness, CarInfo, CarParts, Tool, Column from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries, p16 @@ -68,27 +67,6 @@ class CanBus: camera = 2 -class CAR(StrEnum): - # Global platform - ASCENT = "SUBARU ASCENT LIMITED 2019" - ASCENT_2023 = "SUBARU ASCENT 2023" - IMPREZA = "SUBARU IMPREZA LIMITED 2019" - IMPREZA_2020 = "SUBARU IMPREZA SPORT 2020" - FORESTER = "SUBARU FORESTER 2019" - OUTBACK = "SUBARU OUTBACK 6TH GEN" - CROSSTREK_HYBRID = "SUBARU CROSSTREK HYBRID 2020" - FORESTER_HYBRID = "SUBARU FORESTER HYBRID 2020" - LEGACY = "SUBARU LEGACY 7TH GEN" - FORESTER_2022 = "SUBARU FORESTER 2022" - OUTBACK_2023 = "SUBARU OUTBACK 7TH GEN" - - # Pre-global - FORESTER_PREGLOBAL = "SUBARU FORESTER 2017 - 2018" - LEGACY_PREGLOBAL = "SUBARU LEGACY 2015 - 2018" - OUTBACK_PREGLOBAL = "SUBARU OUTBACK 2015 - 2017" - OUTBACK_PREGLOBAL_2018 = "SUBARU OUTBACK 2018 - 2019" - - class Footnote(Enum): GLOBAL = CarFootnote( "In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance.", @@ -102,7 +80,7 @@ class Footnote(Enum): class SubaruCarInfo(CarInfo): package: str = "EyeSight Driver Assistance" car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.subaru_a])) - footnotes: List[Enum] = field(default_factory=lambda: [Footnote.GLOBAL]) + footnotes: list[Enum] = field(default_factory=lambda: [Footnote.GLOBAL]) def init_make(self, CP: car.CarParams): self.car_parts.parts.extend([Tool.socket_8mm_deep, Tool.pry_tool]) @@ -110,32 +88,107 @@ class SubaruCarInfo(CarInfo): if CP.experimentalLongitudinalAvailable: self.footnotes.append(Footnote.EXP_LONG) -CAR_INFO: Dict[str, Union[SubaruCarInfo, List[SubaruCarInfo]]] = { - CAR.ASCENT: SubaruCarInfo("Subaru Ascent 2019-21", "All"), - CAR.OUTBACK: SubaruCarInfo("Subaru Outback 2020-22", "All", car_parts=CarParts.common([CarHarness.subaru_b])), - CAR.LEGACY: SubaruCarInfo("Subaru Legacy 2020-22", "All", car_parts=CarParts.common([CarHarness.subaru_b])), - CAR.IMPREZA: [ - SubaruCarInfo("Subaru Impreza 2017-19"), - SubaruCarInfo("Subaru Crosstrek 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"), - SubaruCarInfo("Subaru XV 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"), - ], - CAR.IMPREZA_2020: [ - SubaruCarInfo("Subaru Impreza 2020-22"), - SubaruCarInfo("Subaru Crosstrek 2020-23"), - SubaruCarInfo("Subaru XV 2020-21"), - ], + +@dataclass +class SubaruPlatformConfig(PlatformConfig): + dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict('subaru_global_2017_generated', None)) + + +class CAR(Platforms): + # Global platform + ASCENT = SubaruPlatformConfig( + "SUBARU ASCENT LIMITED 2019", + SubaruCarInfo("Subaru Ascent 2019-21", "All"), + specs=CarSpecs(mass=2031, wheelbase=2.89, steerRatio=13.5), + ) + OUTBACK = SubaruPlatformConfig( + "SUBARU OUTBACK 6TH GEN", + SubaruCarInfo("Subaru Outback 2020-22", "All", car_parts=CarParts.common([CarHarness.subaru_b])), + specs=CarSpecs(mass=1568, wheelbase=2.67, steerRatio=17), + ) + LEGACY = SubaruPlatformConfig( + "SUBARU LEGACY 7TH GEN", + SubaruCarInfo("Subaru Legacy 2020-22", "All", car_parts=CarParts.common([CarHarness.subaru_b])), + specs=OUTBACK.specs, + ) + IMPREZA = SubaruPlatformConfig( + "SUBARU IMPREZA LIMITED 2019", + [ + SubaruCarInfo("Subaru Impreza 2017-19"), + SubaruCarInfo("Subaru Crosstrek 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"), + SubaruCarInfo("Subaru XV 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"), + ], + specs=CarSpecs(mass=1568, wheelbase=2.67, steerRatio=15), + ) + IMPREZA_2020 = SubaruPlatformConfig( + "SUBARU IMPREZA SPORT 2020", + [ + SubaruCarInfo("Subaru Impreza 2020-22"), + SubaruCarInfo("Subaru Crosstrek 2020-23"), + SubaruCarInfo("Subaru XV 2020-21"), + ], + specs=CarSpecs(mass=1480, wheelbase=2.67, steerRatio=17), + ) # TODO: is there an XV and Impreza too? - CAR.CROSSTREK_HYBRID: SubaruCarInfo("Subaru Crosstrek Hybrid 2020", car_parts=CarParts.common([CarHarness.subaru_b])), - CAR.FORESTER_HYBRID: SubaruCarInfo("Subaru Forester Hybrid 2020"), - CAR.FORESTER: SubaruCarInfo("Subaru Forester 2019-21", "All"), - CAR.FORESTER_PREGLOBAL: SubaruCarInfo("Subaru Forester 2017-18"), - CAR.LEGACY_PREGLOBAL: SubaruCarInfo("Subaru Legacy 2015-18"), - CAR.OUTBACK_PREGLOBAL: SubaruCarInfo("Subaru Outback 2015-17"), - CAR.OUTBACK_PREGLOBAL_2018: SubaruCarInfo("Subaru Outback 2018-19"), - CAR.FORESTER_2022: SubaruCarInfo("Subaru Forester 2022-23", "All", car_parts=CarParts.common([CarHarness.subaru_c])), - CAR.OUTBACK_2023: SubaruCarInfo("Subaru Outback 2023", "All", car_parts=CarParts.common([CarHarness.subaru_d])), - CAR.ASCENT_2023: SubaruCarInfo("Subaru Ascent 2023", "All", car_parts=CarParts.common([CarHarness.subaru_d])), -} + CROSSTREK_HYBRID = SubaruPlatformConfig( + "SUBARU CROSSTREK HYBRID 2020", + SubaruCarInfo("Subaru Crosstrek Hybrid 2020", car_parts=CarParts.common([CarHarness.subaru_b])), + dbc_dict('subaru_global_2020_hybrid_generated', None), + specs=CarSpecs(mass=1668, wheelbase=2.67, steerRatio=17), + ) + FORESTER = SubaruPlatformConfig( + "SUBARU FORESTER 2019", + SubaruCarInfo("Subaru Forester 2019-21", "All"), + specs=CarSpecs(mass=1668, wheelbase=2.67, steerRatio=17), + ) + FORESTER_HYBRID = SubaruPlatformConfig( + "SUBARU FORESTER HYBRID 2020", + SubaruCarInfo("Subaru Forester Hybrid 2020"), + dbc_dict('subaru_global_2020_hybrid_generated', None), + specs=FORESTER.specs, + ) + # Pre-global + FORESTER_PREGLOBAL = SubaruPlatformConfig( + "SUBARU FORESTER 2017 - 2018", + SubaruCarInfo("Subaru Forester 2017-18"), + dbc_dict('subaru_forester_2017_generated', None), + specs=CarSpecs(mass=1568, wheelbase=2.67, steerRatio=20), + ) + LEGACY_PREGLOBAL = SubaruPlatformConfig( + "SUBARU LEGACY 2015 - 2018", + SubaruCarInfo("Subaru Legacy 2015-18"), + dbc_dict('subaru_outback_2015_generated', None), + specs=CarSpecs(mass=1568, wheelbase=2.67, steerRatio=12.5), + ) + OUTBACK_PREGLOBAL = SubaruPlatformConfig( + "SUBARU OUTBACK 2015 - 2017", + SubaruCarInfo("Subaru Outback 2015-17"), + dbc_dict('subaru_outback_2015_generated', None), + specs=FORESTER_PREGLOBAL.specs, + ) + OUTBACK_PREGLOBAL_2018 = SubaruPlatformConfig( + "SUBARU OUTBACK 2018 - 2019", + SubaruCarInfo("Subaru Outback 2018-19"), + dbc_dict('subaru_outback_2019_generated', None), + specs=FORESTER_PREGLOBAL.specs, + ) + # Angle LKAS + FORESTER_2022 = SubaruPlatformConfig( + "SUBARU FORESTER 2022", + SubaruCarInfo("Subaru Forester 2022-24", "All", car_parts=CarParts.common([CarHarness.subaru_c])), + specs=FORESTER.specs, + ) + OUTBACK_2023 = SubaruPlatformConfig( + "SUBARU OUTBACK 7TH GEN", + SubaruCarInfo("Subaru Outback 2023", "All", car_parts=CarParts.common([CarHarness.subaru_d])), + specs=OUTBACK.specs, + ) + ASCENT_2023 = SubaruPlatformConfig( + "SUBARU ASCENT 2023", + SubaruCarInfo("Subaru Ascent 2023", "All", car_parts=CarParts.common([CarHarness.subaru_d])), + specs=ASCENT.specs, + ) + LKAS_ANGLE = {CAR.FORESTER_2022, CAR.OUTBACK_2023, CAR.ASCENT_2023} GLOBAL_GEN2 = {CAR.OUTBACK, CAR.LEGACY, CAR.OUTBACK_2023, CAR.ASCENT_2023} @@ -186,20 +239,5 @@ FW_QUERY_CONFIG = FwQueryConfig( } ) -DBC = { - CAR.ASCENT: dbc_dict('subaru_global_2017_generated', None), - CAR.ASCENT_2023: dbc_dict('subaru_global_2017_generated', None), - CAR.IMPREZA: dbc_dict('subaru_global_2017_generated', None), - CAR.IMPREZA_2020: dbc_dict('subaru_global_2017_generated', None), - CAR.FORESTER: dbc_dict('subaru_global_2017_generated', None), - CAR.FORESTER_2022: dbc_dict('subaru_global_2017_generated', None), - CAR.OUTBACK: dbc_dict('subaru_global_2017_generated', None), - CAR.FORESTER_HYBRID: dbc_dict('subaru_global_2020_hybrid_generated', None), - CAR.CROSSTREK_HYBRID: dbc_dict('subaru_global_2020_hybrid_generated', None), - CAR.OUTBACK_2023: dbc_dict('subaru_global_2017_generated', None), - CAR.LEGACY: dbc_dict('subaru_global_2017_generated', None), - CAR.FORESTER_PREGLOBAL: dbc_dict('subaru_forester_2017_generated', None), - CAR.LEGACY_PREGLOBAL: dbc_dict('subaru_outback_2015_generated', None), - CAR.OUTBACK_PREGLOBAL: dbc_dict('subaru_outback_2015_generated', None), - CAR.OUTBACK_PREGLOBAL_2018: dbc_dict('subaru_outback_2019_generated', None), -} +CAR_INFO = CAR.create_carinfo_map() +DBC = CAR.create_dbc_map() diff --git a/selfdrive/car/tesla/values.py b/selfdrive/car/tesla/values.py index 12877f1344..2a51d15da8 100644 --- a/selfdrive/car/tesla/values.py +++ b/selfdrive/car/tesla/values.py @@ -1,6 +1,5 @@ from collections import namedtuple from enum import StrEnum -from typing import Dict, List, Union from cereal import car from openpilot.selfdrive.car import AngleRateLimit, dbc_dict @@ -17,7 +16,7 @@ class CAR(StrEnum): AP2_MODELS = 'TESLA AP2 MODEL S' -CAR_INFO: Dict[str, Union[CarInfo, List[CarInfo]]] = { +CAR_INFO: dict[str, CarInfo | list[CarInfo]] = { CAR.AP1_MODELS: CarInfo("Tesla AP1 Model S", "All"), CAR.AP2_MODELS: CarInfo("Tesla AP2 Model S", "All"), } diff --git a/selfdrive/car/tests/big_cars_test.sh b/selfdrive/car/tests/big_cars_test.sh new file mode 100755 index 0000000000..af45c9cd14 --- /dev/null +++ b/selfdrive/car/tests/big_cars_test.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +SCRIPT_DIR=$(dirname "$0") +BASEDIR=$(realpath "$SCRIPT_DIR/../../../") +cd $BASEDIR + +MAX_EXAMPLES=300 +INTERNAL_SEG_CNT=300 +FILEREADER_CACHE=1 +INTERNAL_SEG_LIST=selfdrive/car/tests/test_models_segs.txt + +cd selfdrive/car/tests && pytest test_models.py test_car_interfaces.py \ No newline at end of file diff --git a/selfdrive/car/tests/routes.py b/selfdrive/car/tests/routes.py index 8db950ced0..9c14c0d252 100755 --- a/selfdrive/car/tests/routes.py +++ b/selfdrive/car/tests/routes.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from typing import NamedTuple, Optional +from typing import NamedTuple from openpilot.selfdrive.car.chrysler.values import CAR as CHRYSLER from openpilot.selfdrive.car.gm.values import CAR as GM @@ -20,7 +20,6 @@ non_tested_cars = [ GM.CADILLAC_ATS, GM.HOLDEN_ASTRA, GM.MALIBU, - GM.EQUINOX, HYUNDAI.GENESIS_G90, HONDA.ODYSSEY_CHN, VOLKSWAGEN.CRAFTER_MK2, # need a route from an ACC-equipped Crafter @@ -30,8 +29,8 @@ non_tested_cars = [ class CarTestRoute(NamedTuple): route: str - car_model: Optional[str] - segment: Optional[int] = None + car_model: str | None + segment: int | None = None routes = [ @@ -46,6 +45,7 @@ routes = [ CarTestRoute("3d84727705fecd04|2021-05-25--08-38-56", CHRYSLER.PACIFICA_2020), CarTestRoute("221c253375af4ee9|2022-06-15--18-38-24", CHRYSLER.RAM_1500), CarTestRoute("8fb5eabf914632ae|2022-08-04--17-28-53", CHRYSLER.RAM_HD, segment=6), + CarTestRoute("3379c85aeedc8285|2023-12-07--17-49-39", CHRYSLER.DODGE_DURANGO), CarTestRoute("54827bf84c38b14f|2023-01-25--14-14-11", FORD.BRONCO_SPORT_MK1), CarTestRoute("f8eaaccd2a90aef8|2023-05-04--15-10-09", FORD.ESCAPE_MK4), @@ -59,6 +59,7 @@ routes = [ CarTestRoute("7cc2a8365b4dd8a9|2018-12-02--12-10-44", GM.ACADIA), CarTestRoute("aa20e335f61ba898|2019-02-05--16-59-04", GM.BUICK_REGAL), CarTestRoute("75a6bcb9b8b40373|2023-03-11--22-47-33", GM.BUICK_LACROSSE), + CarTestRoute("e746f59bc96fd789|2024-01-31--22-25-58", GM.EQUINOX), CarTestRoute("ef8f2185104d862e|2023-02-09--18-37-13", GM.ESCALADE), CarTestRoute("46460f0da08e621e|2021-10-26--07-21-46", GM.ESCALADE_ESV), CarTestRoute("168f8b3be57f66ae|2023-09-12--21-44-42", GM.ESCALADE_ESV_2019), @@ -186,6 +187,7 @@ routes = [ CarTestRoute("5f5afb36036506e4|2019-05-14--02-09-54", TOYOTA.COROLLA_TSS2), CarTestRoute("5ceff72287a5c86c|2019-10-19--10-59-02", TOYOTA.COROLLA_TSS2), # hybrid CarTestRoute("d2525c22173da58b|2021-04-25--16-47-04", TOYOTA.PRIUS), + CarTestRoute("b14c5b4742e6fc85|2020-07-28--19-50-11", TOYOTA.RAV4), CarTestRoute("32a7df20486b0f70|2020-02-06--16-06-50", TOYOTA.RAV4H), CarTestRoute("cdf2f7de565d40ae|2019-04-25--03-53-41", TOYOTA.RAV4_TSS2), CarTestRoute("a5c341bb250ca2f0|2022-05-18--16-05-17", TOYOTA.RAV4_TSS2_2022), @@ -207,6 +209,7 @@ routes = [ CarTestRoute("ec429c0f37564e3c|2020-02-01--17-28-12", TOYOTA.LEXUS_NX), # hybrid CarTestRoute("3fd5305f8b6ca765|2021-04-28--19-26-49", TOYOTA.LEXUS_NX_TSS2), CarTestRoute("09ae96064ed85a14|2022-06-09--12-22-31", TOYOTA.LEXUS_NX_TSS2), # hybrid + CarTestRoute("4765fbbf59e3cd88|2024-02-06--17-45-32", TOYOTA.LEXUS_LC_TSS2), CarTestRoute("0a302ffddbb3e3d3|2020-02-08--16-19-08", TOYOTA.HIGHLANDER_TSS2), CarTestRoute("437e4d2402abf524|2021-05-25--07-58-50", TOYOTA.HIGHLANDER_TSS2), # hybrid CarTestRoute("3183cd9b021e89ce|2021-05-25--10-34-44", TOYOTA.HIGHLANDER), @@ -221,7 +224,7 @@ routes = [ CarTestRoute("ea8fbe72b96a185c|2023-02-08--15-11-46", TOYOTA.CHR_TSS2), CarTestRoute("ea8fbe72b96a185c|2023-02-22--09-20-34", TOYOTA.CHR_TSS2), # openpilot longitudinal, with smartDSU CarTestRoute("6719965b0e1d1737|2023-02-09--22-44-05", TOYOTA.CHR_TSS2), # hybrid - # CarTestRoute("6719965b0e1d1737|2023-08-29--06-40-05", TOYOTA.CHR_TSS2), # hybrid, openpilot longitudinal, radar disabled + CarTestRoute("6719965b0e1d1737|2023-08-29--06-40-05", TOYOTA.CHR_TSS2), # hybrid, openpilot longitudinal, radar disabled CarTestRoute("14623aae37e549f3|2021-10-24--01-20-49", TOYOTA.PRIUS_V), CarTestRoute("202c40641158a6e5|2021-09-21--09-43-24", VOLKSWAGEN.ARTEON_MK1), @@ -289,7 +292,6 @@ routes = [ # Segments that test specific issues # Controls mismatch due to interceptor threshold CarTestRoute("cfb32f0fb91b173b|2022-04-06--14-54-45", HONDA.CIVIC, segment=21), - CarTestRoute("5a8762b91fc70467|2022-04-14--21-26-20", TOYOTA.RAV4, segment=2), # Controls mismatch due to standstill threshold CarTestRoute("bec2dcfde6a64235|2022-04-08--14-21-32", HONDA.CRV_HYBRID, segment=22), ] diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index 2306cbf456..a454f616cb 100755 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -46,11 +46,6 @@ def get_fuzzy_car_interface_args(draw: DrawType) -> dict: class TestCarInterfaces(unittest.TestCase): - - @classmethod - def setUpClass(cls): - os.environ['NO_RADAR_SLEEP'] = '1' - # FIXME: Due to the lists used in carParams, Phase.target is very slow and will cause # many generated examples to overrun when max_examples > ~20, don't use it @parameterized.expand([(car,) for car in sorted(all_known_cars())]) @@ -79,6 +74,10 @@ class TestCarInterfaces(unittest.TestCase): self.assertEqual(len(car_params.longitudinalTuning.kiV), len(car_params.longitudinalTuning.kiBP)) self.assertEqual(len(car_params.longitudinalTuning.deadzoneV), len(car_params.longitudinalTuning.deadzoneBP)) + # If we're using the interceptor for gasPressed, we should be commanding gas with it + if car_params.enableGasInterceptor: + self.assertTrue(car_params.openpilotLongitudinalControl) + # Lateral sanity checks if car_params.steerControlType != car.CarParams.SteerControlType.angle: tune = car_params.lateralTuning diff --git a/selfdrive/car/tests/test_docs.py b/selfdrive/car/tests/test_docs.py index d9d67fe87d..0cafb508f7 100755 --- a/selfdrive/car/tests/test_docs.py +++ b/selfdrive/car/tests/test_docs.py @@ -20,7 +20,7 @@ class TestCarDocs(unittest.TestCase): def test_generator(self): generated_cars_md = generate_cars_md(self.all_cars, CARS_MD_TEMPLATE) - with open(CARS_MD_OUT, "r") as f: + with open(CARS_MD_OUT) as f: current_cars_md = f.read() self.assertEqual(generated_cars_md, current_cars_md, @@ -45,7 +45,7 @@ class TestCarDocs(unittest.TestCase): all_car_info_platforms = get_interface_attr("CAR_INFO", combine_brands=True).keys() for platform in sorted(interfaces.keys()): with self.subTest(platform=platform): - self.assertTrue(platform in all_car_info_platforms, "Platform: {} doesn't exist in CarInfo".format(platform)) + self.assertTrue(platform in all_car_info_platforms, f"Platform: {platform} doesn't exist in CarInfo") def test_naming_conventions(self): # Asserts market-standard car naming conventions by brand diff --git a/selfdrive/car/tests/test_fingerprints.py b/selfdrive/car/tests/test_fingerprints.py index 61e9a4d165..34f30bc703 100755 --- a/selfdrive/car/tests/test_fingerprints.py +++ b/selfdrive/car/tests/test_fingerprints.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import os import sys -from typing import Dict, List from openpilot.common.basedir import BASEDIR @@ -64,7 +63,7 @@ def check_can_ignition_conflicts(fingerprints, brands): if __name__ == "__main__": fingerprints = _get_fingerprints() - fingerprints_flat: List[Dict] = [] + fingerprints_flat: list[dict] = [] car_names = [] brand_names = [] for brand in fingerprints: diff --git a/selfdrive/car/tests/test_fw_fingerprint.py b/selfdrive/car/tests/test_fw_fingerprint.py index 064f2e5651..1a745b4447 100755 --- a/selfdrive/car/tests/test_fw_fingerprint.py +++ b/selfdrive/car/tests/test_fw_fingerprint.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import pytest import random import time import unittest @@ -34,18 +33,29 @@ class TestFwFingerprint(unittest.TestCase): self.assertEqual(len(candidates), 1, f"got more than one candidate: {candidates}") self.assertEqual(candidates[0], expected) - @parameterized.expand([(b, c, e[c]) for b, e in VERSIONS.items() for c in e]) - def test_exact_match(self, brand, car_model, ecus): + @parameterized.expand([(b, c, e[c], n) for b, e in VERSIONS.items() for c in e for n in (True, False)]) + def test_exact_match(self, brand, car_model, ecus, test_non_essential): + config = FW_QUERY_CONFIGS[brand] CP = car.CarParams.new_message() - for _ in range(200): + for _ in range(100): fw = [] for ecu, fw_versions in ecus.items(): + # Assume non-essential ECUs apply to all cars, so we catch cases where Car A with + # missing ECUs won't match to Car B where only Car B has labeled non-essential ECUs + if ecu[0] in config.non_essential_ecus and test_non_essential: + continue + ecu_name, addr, sub_addr = ecu fw.append({"ecu": ecu_name, "fwVersion": random.choice(fw_versions), 'brand': brand, "address": addr, "subAddress": 0 if sub_addr is None else sub_addr}) CP.carFw = fw _, matches = match_fw_to_car(CP.carFw, allow_fuzzy=False) - self.assertFingerprints(matches, car_model) + if not test_non_essential: + self.assertFingerprints(matches, car_model) + else: + # if we're removing ECUs we expect some match loss, but it shouldn't mismatch + if len(matches) != 0: + self.assertFingerprints(matches, car_model) @parameterized.expand([(b, c, e[c]) for b, e in VERSIONS.items() for c in e]) def test_custom_fuzzy_match(self, brand, car_model, ecus): @@ -226,7 +236,7 @@ class TestFwFingerprintTiming(unittest.TestCase): def test_startup_timing(self): # Tests worse-case VIN query time and typical present ECU query time - vin_ref_times = {'worst': 1.5, 'best': 0.5} # best assumes we go through all queries to get a match + vin_ref_times = {'worst': 1.2, 'best': 0.6} # best assumes we go through all queries to get a match present_ecu_ref_time = 0.75 def fake_get_ecu_addrs(*_, timeout): @@ -252,48 +262,50 @@ class TestFwFingerprintTiming(unittest.TestCase): self._assert_timing(self.total_time / self.N, vin_ref_times[name]) print(f'get_vin {name} case, query time={self.total_time / self.N} seconds') - @pytest.mark.timeout(60) def test_fw_query_timing(self): - total_ref_time = 6.8 + total_ref_time = {1: 6.5, 2: 7.4} brand_ref_times = { 1: { 'gm': 0.5, 'body': 0.1, 'chrysler': 0.3, - 'ford': 0.1, + 'ford': 0.2, 'honda': 0.55, - 'hyundai': 0.65, + 'hyundai': 1.05, 'mazda': 0.1, 'nissan': 0.8, 'subaru': 0.45, 'tesla': 0.2, 'toyota': 1.6, - 'volkswagen': 0.2, + 'volkswagen': 0.65, }, 2: { - 'ford': 0.2, - 'hyundai': 1.05, + 'ford': 0.3, + 'hyundai': 1.85, } } - total_time = 0 + total_times = {1: 0.0, 2: 0.0} for num_pandas in (1, 2): for brand, config in FW_QUERY_CONFIGS.items(): with self.subTest(brand=brand, num_pandas=num_pandas): - multi_panda_requests = [r for r in config.requests if r.bus > 3] - if not len(multi_panda_requests) and num_pandas > 1: - raise unittest.SkipTest("No multi-panda FW queries") - avg_time = self._benchmark_brand(brand, num_pandas) - total_time += avg_time + total_times[num_pandas] += avg_time avg_time = round(avg_time, 2) - self._assert_timing(avg_time, brand_ref_times[num_pandas][brand]) + + ref_time = brand_ref_times[num_pandas].get(brand) + if ref_time is None: + # ref time should be same as 1 panda if no aux queries + ref_time = brand_ref_times[num_pandas - 1][brand] + + self._assert_timing(avg_time, ref_time) print(f'{brand=}, {num_pandas=}, {len(config.requests)=}, avg FW query time={avg_time} seconds') - with self.subTest(brand='all_brands'): - total_time = round(total_time, 2) - self._assert_timing(total_time, total_ref_time) - print(f'all brands, total FW query time={total_time} seconds') + for num_pandas in (1, 2): + with self.subTest(brand='all_brands', num_pandas=num_pandas): + total_time = round(total_times[num_pandas], 2) + self._assert_timing(total_time, total_ref_time[num_pandas]) + print(f'all brands, total FW query time={total_time} seconds') if __name__ == "__main__": diff --git a/selfdrive/car/tests/test_lateral_limits.py b/selfdrive/car/tests/test_lateral_limits.py index 10e24ff4c5..083cdd5a5e 100755 --- a/selfdrive/car/tests/test_lateral_limits.py +++ b/selfdrive/car/tests/test_lateral_limits.py @@ -3,7 +3,6 @@ from collections import defaultdict import importlib from parameterized import parameterized_class import sys -from typing import DefaultDict, Dict import unittest from openpilot.common.realtime import DT_CTRL @@ -29,7 +28,7 @@ ABOVE_LIMITS_CARS = [ SUBARU.OUTBACK, ] -car_model_jerks: DefaultDict[str, Dict[str, float]] = defaultdict(dict) +car_model_jerks: defaultdict[str, dict[str, float]] = defaultdict(dict) @parameterized_class('car_model', [(c,) for c in sorted(CAR_MODELS)]) diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index 537214d14c..2b29c14f72 100755 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -8,7 +8,6 @@ import unittest from collections import defaultdict, Counter import hypothesis.strategies as st from hypothesis import Phase, given, settings -from typing import List, Optional, Tuple from parameterized import parameterized_class from cereal import messaging, log, car @@ -23,9 +22,8 @@ from openpilot.selfdrive.car.tests.routes import non_tested_cars, routes, CarTes from openpilot.selfdrive.controls.controlsd import Controls from openpilot.selfdrive.test.helpers import read_segment_list from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT -from openpilot.tools.lib.comma_car_segments import get_url -from openpilot.tools.lib.logreader import LogReader -from openpilot.tools.lib.route import Route, SegmentName, RouteName +from openpilot.tools.lib.logreader import LogReader, internal_source, openpilotci_source +from openpilot.tools.lib.route import SegmentName from panda.tests.libpanda import libpanda_py @@ -37,11 +35,11 @@ NUM_JOBS = int(os.environ.get("NUM_JOBS", "1")) JOB_ID = int(os.environ.get("JOB_ID", "0")) INTERNAL_SEG_LIST = os.environ.get("INTERNAL_SEG_LIST", "") INTERNAL_SEG_CNT = int(os.environ.get("INTERNAL_SEG_CNT", "0")) -MAX_EXAMPLES = int(os.environ.get("MAX_EXAMPLES", "50")) +MAX_EXAMPLES = int(os.environ.get("MAX_EXAMPLES", "300")) CI = os.environ.get("CI", None) is not None -def get_test_cases() -> List[Tuple[str, Optional[CarTestRoute]]]: +def get_test_cases() -> list[tuple[str, CarTestRoute | None]]: # build list of test cases test_cases = [] if not len(INTERNAL_SEG_LIST): @@ -51,7 +49,7 @@ def get_test_cases() -> List[Tuple[str, Optional[CarTestRoute]]]: for i, c in enumerate(sorted(all_known_cars())): if i % NUM_JOBS == JOB_ID: - test_cases.extend(sorted((c.value, r) for r in routes_by_car.get(c, (None,)))) + test_cases.extend(sorted((c, r) for r in routes_by_car.get(c, (None,)))) else: segment_list = read_segment_list(os.path.join(BASEDIR, INTERNAL_SEG_LIST)) @@ -66,22 +64,14 @@ def get_test_cases() -> List[Tuple[str, Optional[CarTestRoute]]]: @pytest.mark.slow @pytest.mark.shared_download_cache class TestCarModelBase(unittest.TestCase): - car_model: Optional[str] = None - test_route: Optional[CarTestRoute] = None + car_model: str | None = None + test_route: CarTestRoute | None = None 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]] - elm_frame: Optional[int] - car_safety_mode_frame: Optional[int] - - @classmethod - def get_logreader(cls, seg): - if len(INTERNAL_SEG_LIST): - route_name = RouteName(cls.test_route.route) - return LogReader(f"cd:/{route_name.dongle_id}/{route_name.time_str}/{seg}/rlog.bz2") - else: - return LogReader(get_url(cls.test_route.route, seg)) + elm_frame: int | None + car_safety_mode_frame: int | None @classmethod def get_testing_data_from_logreader(cls, lr): @@ -133,22 +123,26 @@ class TestCarModelBase(unittest.TestCase): if cls.test_route.segment is not None: test_segs = (cls.test_route.segment,) - # Try the primary method first (CI or internal) + is_internal = len(INTERNAL_SEG_LIST) + for seg in test_segs: + segment_range = f"{cls.test_route.route}/{seg}" + try: - lr = cls.get_logreader(seg) + lr = LogReader(segment_range, default_source=internal_source if is_internal else openpilotci_source) return cls.get_testing_data_from_logreader(lr) except Exception: pass # 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): + if not is_internal: cls.test_route_on_bucket = False for seg in test_segs: + segment_range = f"{cls.test_route.route}/{seg}" try: - lr = LogReader(Route(cls.test_route.route).log_paths()[seg]) + lr = LogReader(segment_range) return cls.get_testing_data_from_logreader(lr) except Exception: pass @@ -239,7 +233,6 @@ class TestCarModelBase(unittest.TestCase): self.assertEqual(can_invalid_cnt, 0) def test_radar_interface(self): - os.environ['NO_RADAR_SLEEP'] = "1" RadarInterface = importlib.import_module(f'selfdrive.car.{self.CP.carName}.radar_interface').RadarInterface RI = RadarInterface(self.CP) assert RI @@ -414,7 +407,7 @@ class TestCarModelBase(unittest.TestCase): controls_allowed_prev = False CS_prev = car.CarState.new_message() - checks = defaultdict(lambda: 0) + checks = defaultdict(int) controlsd = Controls(CI=self.CI) controlsd.initialized = True for idx, can in enumerate(self.can_msgs): diff --git a/selfdrive/car/torque_data/override.toml b/selfdrive/car/torque_data/override.toml index 86723efb7b..339fc533e5 100644 --- a/selfdrive/car/torque_data/override.toml +++ b/selfdrive/car/torque_data/override.toml @@ -42,7 +42,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "CHEVROLET BOLT EUV 2022" = [2.0, 2.0, 0.05] "CHEVROLET SILVERADO 1500 2020" = [1.9, 1.9, 0.112] "CHEVROLET TRAILBLAZER 2021" = [1.33, 1.9, 0.16] -"CHEVROLET EQUINOX 2019" = [2.0, 2.0, 0.05] +"CHEVROLET EQUINOX 2019" = [2.5, 2.5, 0.05] "VOLKSWAGEN PASSAT NMS" = [2.5, 2.5, 0.1] "VOLKSWAGEN SHARAN 2ND GEN" = [2.5, 2.5, 0.1] "HYUNDAI SANTA CRUZ 1ST GEN" = [2.7, 2.7, 0.1] diff --git a/selfdrive/car/torque_data/substitute.toml b/selfdrive/car/torque_data/substitute.toml index bab10bc062..6822ef437b 100644 --- a/selfdrive/car/torque_data/substitute.toml +++ b/selfdrive/car/torque_data/substitute.toml @@ -5,12 +5,15 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "MAZDA CX-5 2022" = "MAZDA CX-9 2021" "MAZDA CX-9" = "MAZDA CX-9 2021" +"DODGE DURANGO 2021" = "CHRYSLER PACIFICA 2020" + "TOYOTA ALPHARD 2020" = "TOYOTA SIENNA 2018" "TOYOTA PRIUS v 2017" = "TOYOTA PRIUS 2017" "LEXUS IS 2018" = "LEXUS NX 2018" "LEXUS CT HYBRID 2018" = "LEXUS NX 2018" "LEXUS ES 2018" = "TOYOTA CAMRY 2018" "LEXUS RC 2020" = "LEXUS NX 2020" +"LEXUS LC 2024" = "LEXUS NX 2020" "KIA OPTIMA 4TH GEN" = "HYUNDAI SONATA 2020" "KIA OPTIMA 4TH GEN FACELIFT" = "HYUNDAI SONATA 2020" diff --git a/selfdrive/car/toyota/carcontroller.py b/selfdrive/car/toyota/carcontroller.py index e8ed9cfcb2..343b1d3031 100644 --- a/selfdrive/car/toyota/carcontroller.py +++ b/selfdrive/car/toyota/carcontroller.py @@ -55,10 +55,10 @@ class CarController: apply_steer = apply_meas_steer_torque_limits(new_steer, self.last_steer, CS.out.steeringTorqueEps, self.params) # >100 degree/sec steering fault prevention - self.steer_rate_counter, apply_steer_req = common_fault_avoidance(abs(CS.out.steeringRateDeg) >= MAX_STEER_RATE, CC.latActive, + self.steer_rate_counter, apply_steer_req = common_fault_avoidance(abs(CS.out.steeringRateDeg) >= MAX_STEER_RATE, lat_active, self.steer_rate_counter, MAX_STEER_RATE_FRAMES) - if not CC.latActive: + if not lat_active: apply_steer = 0 # *** steer angle *** diff --git a/selfdrive/car/toyota/carstate.py b/selfdrive/car/toyota/carstate.py index 2461fa9a26..e4ea0d30f9 100644 --- a/selfdrive/car/toyota/carstate.py +++ b/selfdrive/car/toyota/carstate.py @@ -95,6 +95,9 @@ class CarState(CarStateBase): ret.leftBlinker = cp.vl["BLINKERS_STATE"]["TURN_SIGNALS"] == 1 ret.rightBlinker = cp.vl["BLINKERS_STATE"]["TURN_SIGNALS"] == 2 + if self.CP.carFingerprint != CAR.MIRAI: + ret.engineRpm = cp.vl["ENGINE_RPM"]["RPM"] + ret.steeringTorque = cp.vl["STEER_TORQUE_SENSOR"]["STEER_TORQUE_DRIVER"] ret.steeringTorqueEps = cp.vl["STEER_TORQUE_SENSOR"]["STEER_TORQUE_EPS"] * self.eps_torque_scale # we could use the override bit from dbc, but it's triggered at too high torque values @@ -180,6 +183,9 @@ class CarState(CarStateBase): ("STEER_TORQUE_SENSOR", 50), ] + if CP.carFingerprint != CAR.MIRAI: + messages.append(("ENGINE_RPM", 42)) + if CP.carFingerprint in UNSUPPORTED_DSU_CAR: messages.append(("DSU_CRUISE", 5)) messages.append(("PCM_CRUISE_ALT", 1)) diff --git a/selfdrive/car/toyota/fingerprints.py b/selfdrive/car/toyota/fingerprints.py index fc24f0e72c..12a1d46aaf 100644 --- a/selfdrive/car/toyota/fingerprints.py +++ b/selfdrive/car/toyota/fingerprints.py @@ -38,6 +38,7 @@ FW_VERSIONS = { b'F152607180\x00\x00\x00\x00\x00\x00', b'F152641040\x00\x00\x00\x00\x00\x00', b'F152641050\x00\x00\x00\x00\x00\x00', + b'F152641060\x00\x00\x00\x00\x00\x00', b'F152641061\x00\x00\x00\x00\x00\x00', ], (Ecu.dsu, 0x791, None): [ @@ -55,10 +56,12 @@ FW_VERSIONS = { b'\x01896630725100\x00\x00\x00\x00', b'\x01896630725200\x00\x00\x00\x00', b'\x01896630725300\x00\x00\x00\x00', + b'\x01896630725400\x00\x00\x00\x00', b'\x01896630735100\x00\x00\x00\x00', b'\x01896630738000\x00\x00\x00\x00', b'\x02896630724000\x00\x00\x00\x00897CF3302002\x00\x00\x00\x00', b'\x02896630728000\x00\x00\x00\x00897CF3302002\x00\x00\x00\x00', + b'\x02896630734000\x00\x00\x00\x00897CF3305001\x00\x00\x00\x00', b'\x02896630737000\x00\x00\x00\x00897CF3305001\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x750, 0xf): [ @@ -1361,16 +1364,19 @@ FW_VERSIONS = { b'\x018966378G3000\x00\x00\x00\x00', ], (Ecu.engine, 0x7e0, None): [ + b'\x0237881000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x0237887000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02378A0000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02378F4000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.abs, 0x7b0, None): [ b'\x01F152678221\x00\x00\x00\x00\x00\x00', + b'F152678200\x00\x00\x00\x00\x00\x00', b'F152678210\x00\x00\x00\x00\x00\x00', b'F152678211\x00\x00\x00\x00\x00\x00', ], (Ecu.eps, 0x7a1, None): [ + b'8965B78110\x00\x00\x00\x00\x00\x00', b'8965B78120\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x750, 0xf): [ @@ -1383,6 +1389,23 @@ FW_VERSIONS = { b'\x028646F7803100\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', ], }, + CAR.LEXUS_LC_TSS2: { + (Ecu.engine, 0x7e0, None): [ + b'\x0131130000\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.abs, 0x7b0, None): [ + b'F152611390\x00\x00\x00\x00\x00\x00', + ], + (Ecu.eps, 0x7a1, None): [ + b'8965B11091\x00\x00\x00\x00\x00\x00', + ], + (Ecu.fwdRadar, 0x750, 0xf): [ + b'\x018821F6201400\x00\x00\x00\x00', + ], + (Ecu.fwdCamera, 0x750, 0x6d): [ + b'\x028646F1105200\x00\x00\x00\x008646G3304000\x00\x00\x00\x00', + ], + }, CAR.LEXUS_RC: { (Ecu.engine, 0x700, None): [ b'\x01896632461100\x00\x00\x00\x00', @@ -1417,6 +1440,7 @@ FW_VERSIONS = { }, CAR.LEXUS_RX: { (Ecu.engine, 0x700, None): [ + b'\x01896630E36100\x00\x00\x00\x00', b'\x01896630E36200\x00\x00\x00\x00', b'\x01896630E36300\x00\x00\x00\x00', b'\x01896630E37100\x00\x00\x00\x00', diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index 90b2b760fe..988b1b4547 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -181,6 +181,12 @@ class CarInterface(CarInterfaceBase): ret.tireStiffnessFactor = 0.444 # not optimized yet ret.mass = 4070 * CV.LB_TO_KG + elif candidate == CAR.LEXUS_LC_TSS2: + ret.wheelbase = 2.87 + ret.steerRatio = 13.0 + ret.tireStiffnessFactor = 0.444 # not optimized yet + ret.mass = 4500 * CV.LB_TO_KG + elif candidate == CAR.PRIUS_TSS2: ret.wheelbase = 2.70002 # from toyota online sepc. ret.steerRatio = 13.4 # True steerRatio from older prius @@ -219,20 +225,16 @@ class CarInterface(CarInterfaceBase): found_ecus = [fw.ecu for fw in car_fw] ret.enableDsu = len(found_ecus) > 0 and Ecu.dsu not in found_ecus and candidate not in (NO_DSU_CAR | UNSUPPORTED_DSU_CAR) \ and not (ret.flags & ToyotaFlags.SMART_DSU) - ret.enableGasInterceptor = 0x201 in fingerprint[0] - - if ret.enableGasInterceptor: - ret.safetyConfigs[0].safetyParam |= Panda.FLAG_TOYOTA_GAS_INTERCEPTOR # if the smartDSU is detected, openpilot can send ACC_CONTROL and the smartDSU will block it from the DSU or radar. # since we don't yet parse radar on TSS2/TSS-P radar-based ACC cars, gate longitudinal behind experimental toggle use_sdsu = bool(ret.flags & ToyotaFlags.SMART_DSU) if candidate in (RADAR_ACC_CAR | NO_DSU_CAR): - ret.experimentalLongitudinalAvailable = use_sdsu + ret.experimentalLongitudinalAvailable = use_sdsu or candidate in RADAR_ACC_CAR if not use_sdsu: # Disabling radar is only supported on TSS2 radar-ACC cars - if experimental_long and candidate in RADAR_ACC_CAR and False: # TODO: disabling radar isn't supported yet + if experimental_long and candidate in RADAR_ACC_CAR: ret.flags |= ToyotaFlags.DISABLE_RADAR.value else: use_sdsu = use_sdsu and experimental_long @@ -247,10 +249,14 @@ class CarInterface(CarInterfaceBase): # - TSS-P DSU-less cars w/ CAN filter installed (no radar parser yet) ret.openpilotLongitudinalControl = use_sdsu or ret.enableDsu or candidate in (TSS2_CAR - RADAR_ACC_CAR) or bool(ret.flags & ToyotaFlags.DISABLE_RADAR.value) ret.autoResumeSng = ret.openpilotLongitudinalControl and candidate in NO_STOP_TIMER_CAR + ret.enableGasInterceptor = 0x201 in fingerprint[0] and ret.openpilotLongitudinalControl if not ret.openpilotLongitudinalControl: ret.safetyConfigs[0].safetyParam |= Panda.FLAG_TOYOTA_STOCK_LONGITUDINAL + if ret.enableGasInterceptor: + ret.safetyConfigs[0].safetyParam |= Panda.FLAG_TOYOTA_GAS_INTERCEPTOR + # min speed to enable ACC. if car can do stop and go, then set enabling speed # to a negative value, so it won't matter. ret.minEnableSpeed = -1. if (stop_and_go or ret.enableGasInterceptor) else MIN_ACC_SPEED diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 02a9e142d2..011d153f70 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -2,7 +2,6 @@ import re from collections import defaultdict from dataclasses import dataclass, field from enum import Enum, IntFlag, StrEnum -from typing import Dict, List, Set, Union from cereal import car from openpilot.common.conversions import Conversions as CV @@ -81,6 +80,7 @@ class CAR(StrEnum): LEXUS_IS_TSS2 = "LEXUS IS 2023" LEXUS_NX = "LEXUS NX 2018" LEXUS_NX_TSS2 = "LEXUS NX 2020" + LEXUS_LC_TSS2 = "LEXUS LC 2024" LEXUS_RC = "LEXUS RC 2020" LEXUS_RX = "LEXUS RX 2016" LEXUS_RX_TSS2 = "LEXUS RX 2020" @@ -99,7 +99,7 @@ class ToyotaCarInfo(CarInfo): car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.toyota_a])) -CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { +CAR_INFO: dict[str, ToyotaCarInfo | list[ToyotaCarInfo]] = { # Toyota CAR.ALPHARD_TSS2: [ ToyotaCarInfo("Toyota Alphard 2019-20"), @@ -206,6 +206,7 @@ CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { ToyotaCarInfo("Lexus NX 2020-21"), ToyotaCarInfo("Lexus NX Hybrid 2020-21"), ], + CAR.LEXUS_LC_TSS2: ToyotaCarInfo("Lexus LC 2024"), CAR.LEXUS_RC: ToyotaCarInfo("Lexus RC 2018-20"), CAR.LEXUS_RX: [ ToyotaCarInfo("Lexus RX 2016", "Lexus Safety System+"), @@ -251,7 +252,7 @@ STATIC_DSU_MSGS = [ ] -def get_platform_codes(fw_versions: List[bytes]) -> Dict[bytes, Set[bytes]]: +def get_platform_codes(fw_versions: list[bytes]) -> dict[bytes, set[bytes]]: # Returns sub versions in a dict so comparisons can be made within part-platform-major_version combos codes = defaultdict(set) # Optional[part]-platform-major_version: set of sub_version for fw in fw_versions: @@ -295,7 +296,7 @@ def get_platform_codes(fw_versions: List[bytes]) -> Dict[bytes, Set[bytes]]: return dict(codes) -def match_fw_to_car_fuzzy(live_fw_versions, offline_fw_versions) -> Set[str]: +def match_fw_to_car_fuzzy(live_fw_versions, offline_fw_versions) -> set[str]: candidates = set() for candidate, fws in offline_fw_versions.items(): @@ -444,6 +445,7 @@ DBC = { CAR.PRIUS: dbc_dict('toyota_nodsu_pt_generated', 'toyota_adas'), CAR.PRIUS_V: dbc_dict('toyota_new_mc_pt_generated', 'toyota_adas'), CAR.COROLLA: dbc_dict('toyota_new_mc_pt_generated', 'toyota_adas'), + CAR.LEXUS_LC_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), CAR.LEXUS_RC: dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'), CAR.LEXUS_RX: dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'), CAR.LEXUS_RX_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), @@ -480,7 +482,8 @@ EPS_SCALE = defaultdict(lambda: 73, {CAR.PRIUS: 66, CAR.COROLLA: 88, CAR.LEXUS_I # Toyota/Lexus Safety Sense 2.0 and 2.5 TSS2_CAR = {CAR.RAV4_TSS2, CAR.RAV4_TSS2_2022, CAR.RAV4_TSS2_2023, CAR.COROLLA_TSS2, CAR.LEXUS_ES_TSS2, CAR.LEXUS_RX_TSS2, CAR.HIGHLANDER_TSS2, CAR.PRIUS_TSS2, CAR.CAMRY_TSS2, CAR.LEXUS_IS_TSS2, - CAR.MIRAI, CAR.LEXUS_NX_TSS2, CAR.ALPHARD_TSS2, CAR.AVALON_TSS2, CAR.CHR_TSS2} + CAR.MIRAI, CAR.LEXUS_NX_TSS2, CAR.LEXUS_LC_TSS2, CAR.ALPHARD_TSS2, CAR.AVALON_TSS2, + CAR.CHR_TSS2} NO_DSU_CAR = TSS2_CAR | {CAR.CHR, CAR.CAMRY} diff --git a/selfdrive/car/vin.py b/selfdrive/car/vin.py index fe451aa9a9..e668c35f7d 100755 --- a/selfdrive/car/vin.py +++ b/selfdrive/car/vin.py @@ -15,13 +15,14 @@ def is_valid_vin(vin: str): return re.fullmatch(VIN_RE, vin) is not None -def get_vin(logcan, sendcan, buses, timeout=0.1, retry=3, debug=False): +def get_vin(logcan, sendcan, buses, timeout=0.1, retry=2, debug=False): for i in range(retry): for bus in buses: for request, response, valid_buses, vin_addrs, functional_addrs, rx_offset in ( (StdQueries.UDS_VIN_REQUEST, StdQueries.UDS_VIN_RESPONSE, (0, 1), STANDARD_VIN_ADDRS, FUNCTIONAL_ADDRS, 0x8), (StdQueries.OBD_VIN_REQUEST, StdQueries.OBD_VIN_RESPONSE, (0, 1), STANDARD_VIN_ADDRS, FUNCTIONAL_ADDRS, 0x8), (StdQueries.GM_VIN_REQUEST, StdQueries.GM_VIN_RESPONSE, (0,), [0x24b], None, 0x400), # Bolt fwdCamera + (StdQueries.KWP_VIN_REQUEST, StdQueries.KWP_VIN_RESPONSE, (0,), [0x797], None, 0x3), # Nissan Leaf VCM ): if bus not in valid_buses: continue @@ -40,8 +41,8 @@ def get_vin(logcan, sendcan, buses, timeout=0.1, retry=3, debug=False): for addr in vin_addrs: vin = results.get((addr, None)) if vin is not None: - # Ford pads with null bytes - if len(vin) == 24: + # Ford and Nissan pads with null bytes + if len(vin) in (19, 24): vin = re.sub(b'\x00*$', b'', vin) # Honda Bosch response starts with a length, trim to correct length @@ -49,7 +50,7 @@ def get_vin(logcan, sendcan, buses, timeout=0.1, retry=3, debug=False): vin = vin[1:18] cloudlog.error(f"got vin with {request=}") - return get_rx_addr_for_tx_addr(addr), bus, vin.decode() + return get_rx_addr_for_tx_addr(addr, rx_offset=rx_offset), bus, vin.decode() except Exception: cloudlog.exception("VIN query exception") diff --git a/selfdrive/car/volkswagen/fingerprints.py b/selfdrive/car/volkswagen/fingerprints.py index 8e5a0667bd..eab2bc0090 100644 --- a/selfdrive/car/volkswagen/fingerprints.py +++ b/selfdrive/car/volkswagen/fingerprints.py @@ -1043,6 +1043,7 @@ FW_VERSIONS = { ], (Ecu.srs, 0x715, None): [ b'\xf1\x873Q0959655AC\xf1\x890200\xf1\x82\r11120011100010022212110200', + b'\xf1\x873Q0959655AK\xf1\x890306\xf1\x82\r31210031210021033733310331', b'\xf1\x873Q0959655AP\xf1\x890305\xf1\x82\r11110011110011213331312131', b'\xf1\x873Q0959655AQ\xf1\x890200\xf1\x82\r11120011100010312212113100', b'\xf1\x873Q0959655AS\xf1\x890200\xf1\x82\r11120011100010022212110200', @@ -1062,6 +1063,7 @@ FW_VERSIONS = { (Ecu.fwdRadar, 0x757, None): [ b'\xf1\x875Q0907572D \xf1\x890304\xf1\x82\x0101', b'\xf1\x875Q0907572F \xf1\x890400\xf1\x82\x0101', + b'\xf1\x875Q0907572H \xf1\x890620', b'\xf1\x875Q0907572J \xf1\x890654', b'\xf1\x875Q0907572K \xf1\x890402\xf1\x82\x0101', b'\xf1\x875Q0907572P \xf1\x890682', diff --git a/selfdrive/car/volkswagen/interface.py b/selfdrive/car/volkswagen/interface.py index 710e779d0a..544d104d33 100644 --- a/selfdrive/car/volkswagen/interface.py +++ b/selfdrive/car/volkswagen/interface.py @@ -72,14 +72,18 @@ class CarInterface(CarInterfaceBase): # Global lateral tuning defaults, can be overridden per-vehicle - ret.steerActuatorDelay = 0.1 - ret.steerLimitTimer = 0.4 ret.steerRatio = 15.6 # Let the params learner figure this out - ret.lateralTuning.pid.kpBP = [0.] - ret.lateralTuning.pid.kiBP = [0.] - ret.lateralTuning.pid.kf = 0.00006 - ret.lateralTuning.pid.kpV = [0.6] - ret.lateralTuning.pid.kiV = [0.2] + ret.steerLimitTimer = 0.4 + if candidate in PQ_CARS: + ret.steerActuatorDelay = 0.2 + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) + else: + ret.steerActuatorDelay = 0.1 + ret.lateralTuning.pid.kpBP = [0.] + ret.lateralTuning.pid.kiBP = [0.] + ret.lateralTuning.pid.kf = 0.00006 + ret.lateralTuning.pid.kpV = [0.6] + ret.lateralTuning.pid.kiV = [0.2] # Global longitudinal tuning defaults, can be overridden per-vehicle @@ -131,8 +135,6 @@ class CarInterface(CarInterfaceBase): ret.wheelbase = 2.80 ret.minEnableSpeed = 20 * CV.KPH_TO_MS # ACC "basic", no FtS ret.minSteerSpeed = 50 * CV.KPH_TO_MS - ret.steerActuatorDelay = 0.2 - CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) elif candidate == CAR.POLO_MK6: ret.mass = 1230 @@ -142,7 +144,6 @@ class CarInterface(CarInterfaceBase): ret.mass = 1639 ret.wheelbase = 2.92 ret.minSteerSpeed = 50 * CV.KPH_TO_MS - ret.steerActuatorDelay = 0.2 elif candidate == CAR.TAOS_MK1: ret.mass = 1498 diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index 35cb3607ec..e49b9850be 100644 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -1,7 +1,6 @@ from collections import defaultdict, namedtuple from dataclasses import dataclass, field from enum import Enum, IntFlag, StrEnum -from typing import Dict, List, Union from cereal import car from panda.python import uds @@ -151,7 +150,7 @@ class CAR(StrEnum): PQ_CARS = {CAR.PASSAT_NMS, CAR.SHARAN_MK2} -DBC: Dict[str, Dict[str, str]] = defaultdict(lambda: dbc_dict("vw_mqb_2010", None)) +DBC: dict[str, dict[str, str]] = defaultdict(lambda: dbc_dict("vw_mqb_2010", None)) for car_type in PQ_CARS: DBC[car_type] = dbc_dict("vw_golf_mk4", None) @@ -191,7 +190,7 @@ class VWCarInfo(CarInfo): self.car_parts = CarParts([Device.threex_angled_mount, CarHarness.j533]) -CAR_INFO: Dict[str, Union[VWCarInfo, List[VWCarInfo]]] = { +CAR_INFO: dict[str, VWCarInfo | list[VWCarInfo]] = { CAR.ARTEON_MK1: [ VWCarInfo("Volkswagen Arteon 2018-23", video_link="https://youtu.be/FAomFKPFlDA"), VWCarInfo("Volkswagen Arteon R 2020-23", video_link="https://youtu.be/FAomFKPFlDA"), @@ -293,17 +292,24 @@ VOLKSWAGEN_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + VOLKSWAGEN_RX_OFFSET = 0x6a FW_QUERY_CONFIG = FwQueryConfig( - requests=[ + # TODO: add back whitelists after we gather enough data + requests=[request for bus, obd_multiplexing in [(1, True), (1, False), (0, False)] for request in [ Request( [VOLKSWAGEN_VERSION_REQUEST_MULTI], [VOLKSWAGEN_VERSION_RESPONSE], - whitelist_ecus=[Ecu.srs, Ecu.eps, Ecu.fwdRadar], + # whitelist_ecus=[Ecu.srs, Ecu.eps, Ecu.fwdRadar], rx_offset=VOLKSWAGEN_RX_OFFSET, + bus=bus, + logging=(bus != 1 or not obd_multiplexing), + obd_multiplexing=obd_multiplexing, ), Request( [VOLKSWAGEN_VERSION_REQUEST_MULTI], [VOLKSWAGEN_VERSION_RESPONSE], - whitelist_ecus=[Ecu.engine, Ecu.transmission], + # whitelist_ecus=[Ecu.engine, Ecu.transmission], + bus=bus, + logging=(bus != 1 or not obd_multiplexing), + obd_multiplexing=obd_multiplexing, ), - ], + ]], ) diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 7660e5bd3a..bd3dd08179 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -5,28 +5,34 @@ import time import threading from typing import SupportsFloat -from cereal import car, log -from openpilot.common.numpy_fast import clip -from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper, DT_CTRL -from openpilot.common.params import Params import cereal.messaging as messaging + +from cereal import car, log from cereal.visionipc import VisionIpcClient, VisionStreamType -from openpilot.common.conversions import Conversions as CV + from panda import ALTERNATIVE_EXPERIENCE + +from openpilot.common.conversions import Conversions as CV +from openpilot.common.numpy_fast import clip +from openpilot.common.params import Params +from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper, DT_CTRL from openpilot.common.swaglog import cloudlog -from openpilot.system.version import get_short_branch + from openpilot.selfdrive.boardd.boardd import can_list_to_can_capnp from openpilot.selfdrive.car.car_helpers import get_car, get_startup_event, get_one_can +from openpilot.selfdrive.car.interfaces import CarInterfaceBase +from openpilot.selfdrive.controls.lib.alertmanager import AlertManager, set_offroad_alert from openpilot.selfdrive.controls.lib.drive_helpers import VCruiseHelper, clip_curvature +from openpilot.selfdrive.controls.lib.events import Events, ET from openpilot.selfdrive.controls.lib.latcontrol import LatControl, MIN_LATERAL_CONTROL_SPEED -from openpilot.selfdrive.controls.lib.longcontrol import LongControl from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque -from openpilot.selfdrive.controls.lib.events import Events, ET -from openpilot.selfdrive.controls.lib.alertmanager import AlertManager, set_offroad_alert +from openpilot.selfdrive.controls.lib.longcontrol import LongControl from openpilot.selfdrive.controls.lib.vehicle_model import VehicleModel + from openpilot.system.hardware import HARDWARE +from openpilot.system.version import get_short_branch SOFT_DISABLE_TIME = 3 # seconds LDW_MIN_SPEED = 31 * CV.MPH_TO_MS @@ -55,32 +61,19 @@ ACTIVE_STATES = (State.enabled, State.softDisabling, State.overriding) ENABLED_STATES = (State.preEnabled, *ACTIVE_STATES) -class Controls: - def __init__(self, CI=None): - config_realtime_process(4, Priority.CTRL_HIGH) - - # Ensure the current branch is cached, otherwise the first iteration of controlsd lags - self.branch = get_short_branch() - - # Setup sockets - self.pm = messaging.PubMaster(['sendcan', 'controlsState', 'carState', - 'carControl', 'onroadEvents', 'carParams']) - - self.sensor_packets = ["accelerometer", "gyroscope"] - self.camera_packets = ["roadCameraState", "driverCameraState", "wideRoadCameraState"] +class CarD: + CI: CarInterfaceBase + CS: car.CarState - self.log_sock = messaging.sub_sock('androidLog') + def __init__(self, CI=None): self.can_sock = messaging.sub_sock('can', timeout=20) + self.sm = messaging.SubMaster(['pandaStates']) + self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams']) + + self.can_rcv_timeout_counter = 0 # conseuctive timeout count + self.can_rcv_cum_timeout_counter = 0 # cumulative timeout count self.params = Params() - ignore = self.sensor_packets + ['testJoystick'] - if SIMULATION: - ignore += ['driverCameraState', 'managerState'] - self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration', - 'driverMonitoringState', 'longitudinalPlan', 'liveLocationKalman', - 'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters', - 'testJoystick'] + self.camera_packets + self.sensor_packets, - ignore_alive=ignore, ignore_avg_freq=['radarState', 'testJoystick'], ignore_valid=['testJoystick', ]) if CI is None: # wait for one pandaState and one CAN packet @@ -93,25 +86,17 @@ class Controls: else: self.CI, self.CP = CI, CI.CP - self.joystick_mode = self.params.get_bool("JoystickDebugMode") - # set alternative experiences from parameters - self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator") + disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator") self.CP.alternativeExperience = 0 - if not self.disengage_on_accelerator: + if not disengage_on_accelerator: self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS - # read params - self.is_metric = self.params.get_bool("IsMetric") - self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled") - openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle") - - # detect sound card presence and ensure successful init - sounds_available = HARDWARE.get_sound_card_online() - car_recognized = self.CP.carName != 'mock' + openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle") controller_available = self.CI.CC is not None and openpilot_enabled_toggle and not self.CP.dashcamOnly + self.CP.passive = not car_recognized or not controller_available or self.CP.dashcamOnly if self.CP.passive: safety_config = car.CarParams.SafetyConfig.new_message() @@ -129,6 +114,113 @@ class Controls: self.params.put_nonblocking("CarParamsCache", cp_bytes) self.params.put_nonblocking("CarParamsPersistent", cp_bytes) + def initialize(self): + """Initialize CarInterface, once controls are ready""" + self.CI.init(self.CP, self.can_sock, self.pm.sock['sendcan']) + + def state_update(self, CC: car.CarControl): + """carState update loop, driven by can""" + + # TODO: This should not depend on carControl + + # Update carState from CAN + can_strs = messaging.drain_sock_raw(self.can_sock, wait_for_one=True) + self.CS = self.CI.update(CC, can_strs) + + self.sm.update(0) + + can_rcv_valid = len(can_strs) > 0 + + # Check for CAN timeout + if not can_rcv_valid: + self.can_rcv_timeout_counter += 1 + self.can_rcv_cum_timeout_counter += 1 + else: + self.can_rcv_timeout_counter = 0 + + self.can_rcv_timeout = self.can_rcv_timeout_counter >= 5 + + if can_rcv_valid and REPLAY: + self.can_log_mono_time = messaging.log_from_bytes(can_strs[0]).logMonoTime + + return self.CS + + def state_publish(self, car_events): + """carState and carParams publish loop""" + + # TODO: carState should be independent of the event loop + + # carState + cs_send = messaging.new_message('carState') + cs_send.valid = self.CS.canValid + cs_send.carState = self.CS + cs_send.carState.events = car_events + self.pm.send('carState', cs_send) + + # carParams - logged every 50 seconds (> 1 per segment) + if (self.sm.frame % int(50. / DT_CTRL) == 0): + cp_send = messaging.new_message('carParams') + cp_send.valid = True + cp_send.carParams = self.CP + self.pm.send('carParams', cp_send) + + def controls_update(self, CC: car.CarControl): + """control update loop, driven by carControl""" + + # send car controls over can + now_nanos = self.can_log_mono_time if REPLAY else int(time.monotonic() * 1e9) + actuators_output, can_sends = self.CI.apply(CC, now_nanos) + self.pm.send('sendcan', can_list_to_can_capnp(can_sends, msgtype='sendcan', valid=self.CS.canValid)) + + return actuators_output + + +class Controls: + def __init__(self, CI=None): + self.card = CarD(CI) + + self.CP = self.card.CP + self.CI = self.card.CI + + config_realtime_process(4, Priority.CTRL_HIGH) + + # Ensure the current branch is cached, otherwise the first iteration of controlsd lags + self.branch = get_short_branch() + + # Setup sockets + self.pm = messaging.PubMaster(['controlsState', 'carControl', 'onroadEvents']) + + self.sensor_packets = ["accelerometer", "gyroscope"] + self.camera_packets = ["roadCameraState", "driverCameraState", "wideRoadCameraState"] + + self.log_sock = messaging.sub_sock('androidLog') + + self.params = Params() + ignore = self.sensor_packets + ['testJoystick'] + if SIMULATION: + ignore += ['driverCameraState', 'managerState'] + self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration', + 'driverMonitoringState', 'longitudinalPlan', 'liveLocationKalman', + 'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters', + 'testJoystick'] + self.camera_packets + self.sensor_packets, + ignore_alive=ignore, ignore_avg_freq=ignore+['radarState', 'testJoystick'], ignore_valid=['testJoystick', ], + frequency=int(1/DT_CTRL)) + + self.joystick_mode = self.params.get_bool("JoystickDebugMode") + + # read params + self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator") + self.is_metric = self.params.get_bool("IsMetric") + self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled") + openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle") + + # detect sound card presence and ensure successful init + sounds_available = HARDWARE.get_sound_card_online() + + car_recognized = self.CP.carName != 'mock' + + controller_available = self.CI.CC is not None and openpilot_enabled_toggle and not self.CP.dashcamOnly + # cleanup old params if not self.CP.experimentalLongitudinalAvailable: self.params.remove("ExperimentalLongitudinalEnabled") @@ -158,8 +250,6 @@ class Controls: self.soft_disable_timer = 0 self.mismatch_counter = 0 self.cruise_mismatch_counter = 0 - self.can_rcv_timeout_counter = 0 # conseuctive timeout count - self.can_rcv_cum_timeout_counter = 0 # cumulative timeout count self.last_blinker_frame = 0 self.last_steering_pressed_frame = 0 self.distance_traveled = 0 @@ -311,7 +401,8 @@ class Controls: else: safety_mismatch = pandaState.safetyModel not in IGNORED_SAFETY_MODES - if safety_mismatch or pandaState.safetyRxChecksInvalid or self.mismatch_counter >= 200: + # safety mismatch allows some time for boardd to set the safety mode and publish it back from panda + if (safety_mismatch and self.sm.frame*DT_CTRL > 10.) or pandaState.safetyRxChecksInvalid or self.mismatch_counter >= 200: self.events.add(EventName.controlsMismatch) if log.PandaState.FaultType.relayMalfunction in pandaState.faults: @@ -323,7 +414,7 @@ class Controls: num_events = len(self.events) not_running = {p.name for p in self.sm['managerState'].processes if not p.running and p.shouldBeRunning} - if self.sm.rcv_frame['managerState'] and (not_running - IGNORE_PROCESSES): + if self.sm.recv_frame['managerState'] and (not_running - IGNORE_PROCESSES): self.events.add(EventName.processNotRunning) if not_running != self.not_running_prev: cloudlog.event("process_not_running", not_running=not_running, error=True) @@ -346,10 +437,9 @@ class Controls: self.events.add(EventName.canError) # generic catch-all. ideally, a more specific event should be added above instead - can_rcv_timeout = self.can_rcv_timeout_counter >= 5 has_disable_events = self.events.contains(ET.NO_ENTRY) and (self.events.contains(ET.SOFT_DISABLE) or self.events.contains(ET.IMMEDIATE_DISABLE)) no_system_errors = (not has_disable_events) or (len(self.events) == num_events) - if (not self.sm.all_checks() or can_rcv_timeout) and no_system_errors: + if (not self.sm.all_checks() or self.card.can_rcv_timeout) and no_system_errors: if not self.sm.all_alive(): self.events.add(EventName.commIssue) elif not self.sm.all_freq_ok(): @@ -361,7 +451,7 @@ class Controls: 'invalid': [s for s, valid in self.sm.valid.items() if not valid], 'not_alive': [s for s, alive in self.sm.alive.items() if not alive], 'not_freq_ok': [s for s, freq_ok in self.sm.freq_ok.items() if not freq_ok], - 'can_rcv_timeout': can_rcv_timeout, + 'can_rcv_timeout': self.card.can_rcv_timeout, } if logs != self.logged_comm_issue: cloudlog.event("commIssue", error=True, **logs) @@ -380,7 +470,7 @@ class Controls: self.events.add(EventName.paramsdTemporaryError) # conservative HW alert. if the data or frequency are off, locationd will throw an error - if any((self.sm.frame - self.sm.rcv_frame[s])*DT_CTRL > 10. for s in self.sensor_packets): + if any((self.sm.frame - self.sm.recv_frame[s])*DT_CTRL > 10. for s in self.sensor_packets): self.events.add(EventName.sensorDataInvalid) if not REPLAY: @@ -410,9 +500,12 @@ class Controls: # TODO: fix simulator if not SIMULATION or REPLAY: + # Not show in first 1 km to allow for driving out of garage. This event shows after 5 minutes if not self.sm['liveLocationKalman'].gpsOK and self.sm['liveLocationKalman'].inputsOK and (self.distance_traveled > 1000): - # Not show in first 1 km to allow for driving out of garage. This event shows after 5 minutes self.events.add(EventName.noGps) + if self.sm['liveLocationKalman'].gpsOK: + self.distance_traveled = 0 + self.distance_traveled += CS.vEgo * DT_CTRL if self.sm['modelV2'].frameDropPerc > 20: self.events.add(EventName.modeldLagging) @@ -420,17 +513,13 @@ class Controls: def data_sample(self): """Receive data from sockets and update carState""" - # Update carState from CAN - can_strs = messaging.drain_sock_raw(self.can_sock, wait_for_one=True) - CS = self.CI.update(self.CC, can_strs) - if len(can_strs) and REPLAY: - self.can_log_mono_time = messaging.log_from_bytes(can_strs[0]).logMonoTime + CS = self.card.state_update(self.CC) self.sm.update(0) if not self.initialized: all_valid = CS.canValid and self.sm.all_checks() - timed_out = self.sm.frame * DT_CTRL > (6. if REPLAY else 3.5) + timed_out = self.sm.frame * DT_CTRL > 6. if all_valid or timed_out or (SIMULATION and not REPLAY): available_streams = VisionIpcClient.available_streams("camerad", block=False) if VisionStreamType.VISION_STREAM_ROAD not in available_streams: @@ -439,18 +528,22 @@ class Controls: self.sm.ignore_alive.append('wideRoadCameraState') if not self.CP.passive: - self.CI.init(self.CP, self.can_sock, self.pm.sock['sendcan']) + self.card.initialize() self.initialized = True self.set_initial_state() self.params.put_bool_nonblocking("ControlsReady", True) - # Check for CAN timeout - if not can_strs: - self.can_rcv_timeout_counter += 1 - self.can_rcv_cum_timeout_counter += 1 - else: - self.can_rcv_timeout_counter = 0 + cloudlog.event( + "controlsd.initialized", + dt=self.sm.frame*DT_CTRL, + timeout=timed_out, + canValid=CS.canValid, + invalid=[s for s, valid in self.sm.valid.items() if not valid], + not_alive=[s for s, alive in self.sm.alive.items() if not alive], + not_freq_ok=[s for s, freq_ok in self.sm.freq_ok.items() if not freq_ok], + error=True, + ) # When the panda and controlsd do not agree on controls_allowed # we want to disengage openpilot. However the status from the panda goes through @@ -464,8 +557,6 @@ class Controls: if ps.safetyModel not in IGNORED_SAFETY_MODES): self.mismatch_counter += 1 - self.distance_traveled += CS.vEgo * DT_CTRL - return CS def state_transition(self, CS): @@ -603,7 +694,7 @@ class Controls: if not self.joystick_mode: # accel PID loop pid_accel_limits = self.CI.get_pid_accel_limits(self.CP, CS.vEgo, self.v_cruise_helper.v_cruise_kph * CV.KPH_TO_MS) - t_since_plan = (self.sm.frame - self.sm.rcv_frame['longitudinalPlan']) * DT_CTRL + t_since_plan = (self.sm.frame - self.sm.recv_frame['longitudinalPlan']) * DT_CTRL actuators.accel = self.LoC.update(CC.longActive, CS, long_plan, pid_accel_limits, t_since_plan) # Steering PID loop and lateral MPC @@ -614,9 +705,9 @@ class Controls: self.sm['liveLocationKalman']) else: lac_log = log.ControlsState.LateralDebugState.new_message() - if self.sm.rcv_frame['testJoystick'] > 0: + if self.sm.recv_frame['testJoystick'] > 0: # reset joystick if it hasn't been received in a while - should_reset_joystick = (self.sm.frame - self.sm.rcv_frame['testJoystick'])*DT_CTRL > 0.2 + should_reset_joystick = (self.sm.frame - self.sm.recv_frame['testJoystick'])*DT_CTRL > 0.2 if not should_reset_joystick: joystick_axes = self.sm['testJoystick'].axes else: @@ -691,7 +782,7 @@ class Controls: CC.cruiseControl.override = self.enabled and not CC.longActive and self.CP.openpilotLongitudinalControl CC.cruiseControl.cancel = CS.cruiseState.enabled and (not self.enabled or not self.CP.pcmCruise) - if self.joystick_mode and self.sm.rcv_frame['testJoystick'] > 0 and self.sm['testJoystick'].buttons[0]: + if self.joystick_mode and self.sm.recv_frame['testJoystick'] > 0 and self.sm['testJoystick'].buttons[0]: CC.cruiseControl.cancel = True speeds = self.sm['longitudinalPlan'].speeds @@ -742,10 +833,7 @@ class Controls: hudControl.visualAlert = current_alert.visual_alert if not self.CP.passive and self.initialized: - # send car controls over can - now_nanos = self.can_log_mono_time if REPLAY else int(time.monotonic() * 1e9) - self.last_actuators, can_sends = self.CI.apply(CC, now_nanos) - self.pm.send('sendcan', can_list_to_can_capnp(can_sends, msgtype='sendcan', valid=CS.canValid)) + self.last_actuators = self.card.controls_update(CC) CC.actuatorsOutput = self.last_actuators if self.CP.steerControlType == car.CarParams.SteerControlType.angle: self.steer_limited = abs(CC.actuators.steeringAngleDeg - CC.actuatorsOutput.steeringAngleDeg) > \ @@ -793,7 +881,7 @@ class Controls: controlsState.cumLagMs = -self.rk.remaining * 1000. controlsState.startMonoTime = int(start_time * 1e9) controlsState.forceDecel = bool(force_decel) - controlsState.canErrorCounter = self.can_rcv_cum_timeout_counter + controlsState.canErrorCounter = self.card.can_rcv_cum_timeout_counter controlsState.experimentalMode = self.experimental_mode lat_tuning = self.CP.lateralTuning.which() @@ -808,13 +896,9 @@ class Controls: self.pm.send('controlsState', dat) - # carState car_events = self.events.to_msg() - cs_send = messaging.new_message('carState') - cs_send.valid = CS.canValid - cs_send.carState = CS - cs_send.carState.events = car_events - self.pm.send('carState', cs_send) + + self.card.state_publish(car_events) # onroadEvents - logged every second or on change if (self.sm.frame % int(1. / DT_CTRL) == 0) or (self.events.names != self.events_prev): @@ -824,13 +908,6 @@ class Controls: self.pm.send('onroadEvents', ce_send) self.events_prev = self.events.names.copy() - # carParams - logged every 50 seconds (> 1 per segment) - if (self.sm.frame % int(50. / DT_CTRL) == 0): - cp_send = messaging.new_message('carParams') - cp_send.valid = True - cp_send.carParams = self.CP - self.pm.send('carParams', cp_send) - # carControl cc_send = messaging.new_message('carControl') cc_send.valid = CS.canValid diff --git a/selfdrive/controls/lib/alertmanager.py b/selfdrive/controls/lib/alertmanager.py index 6abcf4cbba..8034c8ebbc 100644 --- a/selfdrive/controls/lib/alertmanager.py +++ b/selfdrive/controls/lib/alertmanager.py @@ -3,7 +3,6 @@ import os import json from collections import defaultdict from dataclasses import dataclass -from typing import List, Dict, Optional from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params @@ -14,7 +13,7 @@ with open(os.path.join(BASEDIR, "selfdrive/controls/lib/alerts_offroad.json")) a OFFROAD_ALERTS = json.load(f) -def set_offroad_alert(alert: str, show_alert: bool, extra_text: Optional[str] = None) -> None: +def set_offroad_alert(alert: str, show_alert: bool, extra_text: str | None = None) -> None: if show_alert: a = copy.copy(OFFROAD_ALERTS[alert]) a['extra'] = extra_text or '' @@ -25,7 +24,7 @@ def set_offroad_alert(alert: str, show_alert: bool, extra_text: Optional[str] = @dataclass class AlertEntry: - alert: Optional[Alert] = None + alert: Alert | None = None start_frame: int = -1 end_frame: int = -1 @@ -34,9 +33,9 @@ class AlertEntry: class AlertManager: def __init__(self): - self.alerts: Dict[str, AlertEntry] = defaultdict(AlertEntry) + self.alerts: dict[str, AlertEntry] = defaultdict(AlertEntry) - def add_many(self, frame: int, alerts: List[Alert]) -> None: + def add_many(self, frame: int, alerts: list[Alert]) -> None: for alert in alerts: entry = self.alerts[alert.alert_type] entry.alert = alert @@ -45,7 +44,7 @@ class AlertManager: min_end_frame = entry.start_frame + alert.duration entry.end_frame = max(frame + 1, min_end_frame) - def process_alerts(self, frame: int, clear_event_types: set) -> Optional[Alert]: + def process_alerts(self, frame: int, clear_event_types: set) -> Alert | None: current_alert = AlertEntry() for v in self.alerts.values(): if not v.alert: diff --git a/selfdrive/controls/lib/events.py b/selfdrive/controls/lib/events.py index b366728094..c5228ef7f2 100755 --- a/selfdrive/controls/lib/events.py +++ b/selfdrive/controls/lib/events.py @@ -2,7 +2,7 @@ import math import os from enum import IntEnum -from typing import Dict, Union, Callable, List, Optional +from collections.abc import Callable from cereal import log, car import cereal.messaging as messaging @@ -48,12 +48,12 @@ EVENT_NAME = {v: k for k, v in EventName.schema.enumerants.items()} class Events: def __init__(self): - self.events: List[int] = [] - self.static_events: List[int] = [] + self.events: list[int] = [] + self.static_events: list[int] = [] self.events_prev = dict.fromkeys(EVENTS.keys(), 0) @property - def names(self) -> List[int]: + def names(self) -> list[int]: return self.events def __len__(self) -> int: @@ -71,7 +71,7 @@ class Events: def contains(self, event_type: str) -> bool: return any(event_type in EVENTS.get(e, {}) for e in self.events) - def create_alerts(self, event_types: List[str], callback_args=None): + def create_alerts(self, event_types: list[str], callback_args=None): if callback_args is None: callback_args = [] @@ -132,7 +132,7 @@ class Alert: self.creation_delay = creation_delay self.alert_type = "" - self.event_type: Optional[str] = None + self.event_type: str | None = None def __str__(self) -> str: return f"{self.alert_text_1}/{self.alert_text_2} {self.priority} {self.visual_alert} {self.audible_alert}" @@ -333,7 +333,7 @@ def joystick_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, -EVENTS: Dict[int, Dict[str, Union[Alert, AlertCallbackType]]] = { +EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { # ********** events with no alerts ********** EventName.stockFcw: {}, @@ -767,12 +767,12 @@ EVENTS: Dict[int, Dict[str, Union[Alert, AlertCallbackType]]] = { # is thrown. This can mean a service crashed, did not broadcast a message for # ten times the regular interval, or the average interval is more than 10% too high. EventName.commIssue: { - ET.SOFT_DISABLE: soft_disable_alert("Communication Issue between Processes"), + ET.SOFT_DISABLE: soft_disable_alert("Communication Issue Between Processes"), ET.NO_ENTRY: comm_issue_alert, }, EventName.commIssueAvgFreq: { - ET.SOFT_DISABLE: soft_disable_alert("Low Communication Rate between Processes"), - ET.NO_ENTRY: NoEntryAlert("Low Communication Rate between Processes"), + ET.SOFT_DISABLE: soft_disable_alert("Low Communication Rate Between Processes"), + ET.NO_ENTRY: NoEntryAlert("Low Communication Rate Between Processes"), }, EventName.controlsdLagging: { @@ -965,7 +965,7 @@ if __name__ == '__main__': from collections import defaultdict event_names = {v: k for k, v in EventName.schema.enumerants.items()} - alerts_by_type: Dict[str, Dict[Priority, List[str]]] = defaultdict(lambda: defaultdict(list)) + alerts_by_type: dict[str, dict[Priority, list[str]]] = defaultdict(lambda: defaultdict(list)) CP = car.CarParams.new_message() CS = car.CarState.new_message() @@ -977,7 +977,7 @@ if __name__ == '__main__': alert = alert(CP, CS, sm, False, 1) alerts_by_type[et][alert.priority].append(event_names[i]) - all_alerts: Dict[str, List[tuple[Priority, List[str]]]] = {} + all_alerts: dict[str, list[tuple[Priority, list[str]]]] = {} for et, priority_alerts in alerts_by_type.items(): all_alerts[et] = sorted(priority_alerts.items(), key=lambda x: x[0], reverse=True) diff --git a/selfdrive/controls/lib/vehicle_model.py b/selfdrive/controls/lib/vehicle_model.py index 0750384918..b6f50b4ba8 100755 --- a/selfdrive/controls/lib/vehicle_model.py +++ b/selfdrive/controls/lib/vehicle_model.py @@ -12,7 +12,6 @@ x_dot = A*x + B*u A depends on longitudinal speed, u [m/s], and vehicle parameters CP """ -from typing import Tuple import numpy as np from numpy.linalg import solve @@ -169,7 +168,7 @@ def kin_ss_sol(sa: float, u: float, VM: VehicleModel) -> np.ndarray: return K * sa -def create_dyn_state_matrices(u: float, VM: VehicleModel) -> Tuple[np.ndarray, np.ndarray]: +def create_dyn_state_matrices(u: float, VM: VehicleModel) -> tuple[np.ndarray, np.ndarray]: """Returns the A and B matrix for the dynamics system Args: diff --git a/selfdrive/controls/plannerd.py b/selfdrive/controls/plannerd.py index 5ab4894424..eeeeda050e 100755 --- a/selfdrive/controls/plannerd.py +++ b/selfdrive/controls/plannerd.py @@ -29,11 +29,10 @@ def plannerd_thread(): longitudinal_planner = LongitudinalPlanner(CP) pm = messaging.PubMaster(['longitudinalPlan', 'uiPlan']) sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'radarState', 'modelV2'], - poll=['radarState', 'modelV2'], ignore_avg_freq=['radarState']) + poll='modelV2', ignore_avg_freq=['radarState']) while True: sm.update() - if sm.updated['modelV2']: longitudinal_planner.update(sm) longitudinal_planner.publish(sm, pm) diff --git a/selfdrive/controls/radard.py b/selfdrive/controls/radard.py index 4acadee7a7..4de4208e9d 100755 --- a/selfdrive/controls/radard.py +++ b/selfdrive/controls/radard.py @@ -2,13 +2,13 @@ import importlib import math from collections import deque -from typing import Optional, Dict, Any +from typing import Any, Optional import capnp from cereal import messaging, log, car from openpilot.common.numpy_fast import interp from openpilot.common.params import Params -from openpilot.common.realtime import Ratekeeper, Priority, config_realtime_process +from openpilot.common.realtime import DT_CTRL, Ratekeeper, Priority, config_realtime_process from openpilot.common.swaglog import cloudlog from openpilot.common.simple_kalman import KF1D @@ -125,7 +125,7 @@ def laplacian_pdf(x: float, mu: float, b: float): return math.exp(-abs(x-mu)/b) -def match_vision_to_track(v_ego: float, lead: capnp._DynamicStructReader, tracks: Dict[int, Track]): +def match_vision_to_track(v_ego: float, lead: capnp._DynamicStructReader, tracks: dict[int, Track]): offset_vision_dist = lead.x[0] - RADAR_TO_CAMERA def prob(c): @@ -166,8 +166,8 @@ def get_RadarState_from_vision(lead_msg: capnp._DynamicStructReader, v_ego: floa } -def get_lead(v_ego: float, ready: bool, tracks: Dict[int, Track], lead_msg: capnp._DynamicStructReader, - model_v_ego: float, low_speed_override: bool = True) -> Dict[str, Any]: +def get_lead(v_ego: float, ready: bool, tracks: dict[int, Track], lead_msg: capnp._DynamicStructReader, + model_v_ego: float, low_speed_override: bool = True) -> dict[str, Any]: # Determine leads, this is where the essential logic happens if len(tracks) > 0 and ready and lead_msg.prob > .5: track = match_vision_to_track(v_ego, lead_msg, tracks) @@ -196,18 +196,20 @@ class RadarD: def __init__(self, radar_ts: float, delay: int = 0): self.current_time = 0.0 - self.tracks: Dict[int, Track] = {} + self.tracks: dict[int, Track] = {} self.kalman_params = KalmanParams(radar_ts) self.v_ego = 0.0 self.v_ego_hist = deque([0.0], maxlen=delay+1) + self.last_v_ego_frame = -1 - self.radar_state: Optional[capnp._DynamicStructBuilder] = None + self.radar_state: capnp._DynamicStructBuilder | None = None self.radar_state_valid = False self.ready = False def update(self, sm: messaging.SubMaster, rr: Optional[car.RadarData]): + self.ready = sm.seen['modelV2'] self.current_time = 1e-9*max(sm.logMonoTime.values()) radar_points = [] @@ -216,11 +218,10 @@ class RadarD: radar_points = rr.points radar_errors = rr.errors - if sm.updated['carState']: + if sm.recv_frame['carState'] != self.last_v_ego_frame: self.v_ego = sm['carState'].vEgo self.v_ego_hist.append(self.v_ego) - if sm.updated['modelV2']: - self.ready = True + self.last_v_ego_frame = sm.recv_frame['carState'] ar_pts = {} for pt in radar_points: @@ -282,7 +283,7 @@ class RadarD: # fuses camera and radar data for best lead detection -def radard_thread(sm: Optional[messaging.SubMaster] = None, pm: Optional[messaging.PubMaster] = None, can_sock: Optional[messaging.SubSocket] = None): +def main(): config_realtime_process(5, Priority.CTRL_LOW) # wait for stats about the car to come in from controls @@ -296,12 +297,9 @@ def radard_thread(sm: Optional[messaging.SubMaster] = None, pm: Optional[messagi RadarInterface = importlib.import_module(f'selfdrive.car.{CP.carName}.radar_interface').RadarInterface # *** setup messaging - if can_sock is None: - can_sock = messaging.sub_sock('can') - if sm is None: - sm = messaging.SubMaster(['modelV2', 'carState'], ignore_avg_freq=['modelV2', 'carState']) # Can't check average frequency, since radar determines timing - if pm is None: - pm = messaging.PubMaster(['radarState', 'liveTracks']) + can_sock = messaging.sub_sock('can') + sm = messaging.SubMaster(['modelV2', 'carState'], frequency=int(1./DT_CTRL)) + pm = messaging.PubMaster(['radarState', 'liveTracks']) RI = RadarInterface(CP) @@ -311,21 +309,15 @@ def radard_thread(sm: Optional[messaging.SubMaster] = None, pm: Optional[messagi while 1: can_strings = messaging.drain_sock_raw(can_sock, wait_for_one=True) rr = RI.update(can_strings) - + sm.update(0) if rr is None: continue - sm.update(0) - RD.update(sm, rr) RD.publish(pm, -rk.remaining*1000.0) rk.monitor_time() -def main(sm: Optional[messaging.SubMaster] = None, pm: Optional[messaging.PubMaster] = None, can_sock: messaging.SubSocket = None): - radard_thread(sm, pm, can_sock) - - if __name__ == "__main__": main() diff --git a/selfdrive/debug/can_print_changes.py b/selfdrive/debug/can_print_changes.py index 810e45cfc6..97d60b2b05 100755 --- a/selfdrive/debug/can_print_changes.py +++ b/selfdrive/debug/can_print_changes.py @@ -3,7 +3,6 @@ import argparse import binascii import time from collections import defaultdict -from typing import Optional import cereal.messaging as messaging from openpilot.selfdrive.debug.can_table import can_table @@ -96,8 +95,8 @@ if __name__ == "__main__": args = parser.parse_args() - init_lr: Optional[LogIterable] = None - new_lr: Optional[LogIterable] = None + init_lr: LogIterable | None = None + new_lr: LogIterable | None = None if args.init: if args.init == '': diff --git a/selfdrive/debug/check_freq.py b/selfdrive/debug/check_freq.py index 7e7b05e950..1765aeb86b 100755 --- a/selfdrive/debug/check_freq.py +++ b/selfdrive/debug/check_freq.py @@ -3,7 +3,7 @@ import argparse import numpy as np import time from collections import defaultdict, deque -from typing import DefaultDict, Deque, MutableSequence +from collections.abc import MutableSequence import cereal.messaging as messaging @@ -19,8 +19,8 @@ if __name__ == "__main__": socket_names = args.socket sockets = {} - rcv_times: DefaultDict[str, MutableSequence[float]] = defaultdict(lambda: deque(maxlen=100)) - valids: DefaultDict[str, Deque[bool]] = defaultdict(lambda: deque(maxlen=100)) + rcv_times: defaultdict[str, MutableSequence[float]] = defaultdict(lambda: deque(maxlen=100)) + valids: defaultdict[str, deque[bool]] = defaultdict(lambda: deque(maxlen=100)) t = time.monotonic() for name in socket_names: diff --git a/selfdrive/debug/check_lag.py b/selfdrive/debug/check_lag.py index 72d11d6eda..341ae79c8b 100755 --- a/selfdrive/debug/check_lag.py +++ b/selfdrive/debug/check_lag.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -from typing import Dict import cereal.messaging as messaging from cereal.services import SERVICE_LIST @@ -10,7 +9,7 @@ TO_CHECK = ['carState'] if __name__ == "__main__": sm = messaging.SubMaster(TO_CHECK) - prev_t: Dict[str, float] = {} + prev_t: dict[str, float] = {} while True: sm.update() diff --git a/selfdrive/debug/check_timings.py b/selfdrive/debug/check_timings.py index fb8467a3c4..3de6b8eb66 100755 --- a/selfdrive/debug/check_timings.py +++ b/selfdrive/debug/check_timings.py @@ -3,13 +3,13 @@ import sys import time import numpy as np -from typing import DefaultDict, MutableSequence +from collections.abc import MutableSequence from collections import defaultdict, deque import cereal.messaging as messaging socks = {s: messaging.sub_sock(s, conflate=False) for s in sys.argv[1:]} -ts: DefaultDict[str, MutableSequence[float]] = defaultdict(lambda: deque(maxlen=100)) +ts: defaultdict[str, MutableSequence[float]] = defaultdict(lambda: deque(maxlen=100)) if __name__ == "__main__": while True: diff --git a/selfdrive/debug/count_events.py b/selfdrive/debug/count_events.py index 0d545b3153..5942054757 100755 --- a/selfdrive/debug/count_events.py +++ b/selfdrive/debug/count_events.py @@ -4,54 +4,58 @@ import math import datetime from collections import Counter from pprint import pprint -from typing import List, Tuple, cast +from typing import cast from cereal.services import SERVICE_LIST from openpilot.tools.lib.logreader import LogReader, ReadMode if __name__ == "__main__": - cnt_valid: Counter = Counter() cnt_events: Counter = Counter() cams = [s for s in SERVICE_LIST if s.endswith('CameraState')] cnt_cameras = dict.fromkeys(cams, 0) - alerts: List[Tuple[float, str]] = [] + events: list[tuple[float, set[str]]] = [] + alerts: list[tuple[float, str]] = [] start_time = math.inf end_time = -math.inf ignition_off = None for msg in LogReader(sys.argv[1], ReadMode.QLOG): + t = (msg.logMonoTime - start_time) / 1e9 end_time = max(end_time, msg.logMonoTime) start_time = min(start_time, msg.logMonoTime) if msg.which() == 'onroadEvents': for e in msg.onroadEvents: cnt_events[e.name] += 1 + + ae = {str(e.name) for e in msg.onroadEvents if e.name not in ('pedalPressed', 'steerOverride', 'gasPressedOverride')} + if len(events) == 0 or ae != events[-1][1]: + events.append((t, ae)) + elif msg.which() == 'controlsState': 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 alerts.append((t, at)) elif msg.which() == 'pandaStates': if ignition_off is None: ign = any(ps.ignitionLine or ps.ignitionCan for ps in msg.pandaStates) if not ign: ignition_off = msg.logMonoTime + break elif msg.which() in cams: cnt_cameras[msg.which()] += 1 - if not msg.valid: - cnt_valid[msg.which()] += 1 - duration = (end_time - start_time) / 1e9 print("Events") pprint(cnt_events) print("\n") - print("Not valid") - pprint(cnt_valid) + print("Events") + for t, evt in events: + print(f"{t:8.2f} {evt}") print("\n") print("Cameras") @@ -64,9 +68,9 @@ if __name__ == "__main__": print("Alerts") for t, a in alerts: print(f"{t:8.2f} {a}") + + print("\n") if ignition_off is not None: ignition_off = round((ignition_off - start_time) / 1e9, 2) print("Ignition off at", ignition_off) - - print("\n") print("Route duration", datetime.timedelta(seconds=duration)) diff --git a/selfdrive/debug/cpu_usage_stat.py b/selfdrive/debug/cpu_usage_stat.py index 9050fbb064..ec9d02e8f4 100755 --- a/selfdrive/debug/cpu_usage_stat.py +++ b/selfdrive/debug/cpu_usage_stat.py @@ -66,12 +66,12 @@ if __name__ == "__main__": for p in psutil.process_iter(): if p == psutil.Process(): continue - matched = any(l for l in p.cmdline() if any(pn for pn in monitored_proc_names if re.match(r'.*{}.*'.format(pn), l, re.M | re.I))) + matched = any(l for l in p.cmdline() if any(pn for pn in monitored_proc_names if re.match(fr'.*{pn}.*', l, re.M | re.I))) if matched: k = ' '.join(p.cmdline()) print('Add monitored proc:', k) stats[k] = {'cpu_samples': defaultdict(list), 'min': defaultdict(lambda: None), 'max': defaultdict(lambda: None), - 'avg': defaultdict(lambda: 0.0), 'last_cpu_times': None, 'last_sys_time': None} + 'avg': defaultdict(float), 'last_cpu_times': None, 'last_sys_time': None} stats[k]['last_sys_time'] = timer() stats[k]['last_cpu_times'] = p.cpu_times() monitored_procs.append(p) diff --git a/selfdrive/debug/dump.py b/selfdrive/debug/dump.py index db18f4c622..787e9bc738 100755 --- a/selfdrive/debug/dump.py +++ b/selfdrive/debug/dump.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 -import os import sys import argparse import json import codecs -import cereal.messaging as messaging from hexdump import hexdump from cereal import log from cereal.services import SERVICE_LIST +from openpilot.tools.lib.live_logreader import raw_live_logreader + codecs.register_error("strict", codecs.backslashreplace_errors) @@ -22,32 +22,20 @@ if __name__ == "__main__": parser.add_argument('--no-print', action='store_true') parser.add_argument('--addr', default='127.0.0.1') parser.add_argument('--values', help='values to monitor (instead of entire event)') - parser.add_argument("socket", type=str, nargs='*', help="socket names to dump. defaults to all services defined in cereal") + parser.add_argument("socket", type=str, nargs='*', default=list(SERVICE_LIST.keys()), help="socket names to dump. defaults to all services defined in cereal") args = parser.parse_args() - if args.addr != "127.0.0.1": - os.environ["ZMQ"] = "1" - messaging.context = messaging.Context() - - poller = messaging.Poller() - - for m in args.socket if len(args.socket) > 0 else SERVICE_LIST: - messaging.sub_sock(m, poller, addr=args.addr) + lr = raw_live_logreader(args.socket, args.addr) values = None if args.values: values = [s.strip().split(".") for s in args.values.split(",")] - while 1: - polld = poller.poll(100) - for sock in polld: - msg = sock.receive() - with log.Event.from_bytes(msg) as log_evt: - evt = log_evt - + for msg in lr: + with log.Event.from_bytes(msg) as evt: if not args.no_print: if args.pipe: - sys.stdout.write(msg) + sys.stdout.write(str(msg)) sys.stdout.flush() elif args.raw: hexdump(msg) diff --git a/selfdrive/debug/filter_log_message.py b/selfdrive/debug/filter_log_message.py index 20028f8fd2..9cbab0b41f 100755 --- a/selfdrive/debug/filter_log_message.py +++ b/selfdrive/debug/filter_log_message.py @@ -46,6 +46,7 @@ def print_androidlog(t, msg): if __name__ == "__main__": parser = argparse.ArgumentParser() + parser.add_argument('--absolute', action='store_true') parser.add_argument('--level', default='DEBUG') parser.add_argument('--addr', default='127.0.0.1') parser.add_argument("route", type=str, nargs='*', help="route name + segment number for offline usage") @@ -54,15 +55,18 @@ if __name__ == "__main__": min_level = LEVELS[args.level] if args.route: + st = None if not args.absolute else 0 for route in args.route: - lr = LogReader(route) + lr = LogReader(route, sort_by_time=True) for m in lr: + if st is None: + st = m.logMonoTime if m.which() == 'logMessage': - print_logmessage(m.logMonoTime, m.logMessage, min_level) + print_logmessage(m.logMonoTime-st, m.logMessage, min_level) elif m.which() == 'errorLogMessage': - print_logmessage(m.logMonoTime, m.errorLogMessage, min_level) + print_logmessage(m.logMonoTime-st, m.errorLogMessage, min_level) elif m.which() == 'androidLog': - print_androidlog(m.logMonoTime, m.androidLog) + print_androidlog(m.logMonoTime-st, m.androidLog) else: sm = messaging.SubMaster(['logMessage', 'androidLog'], addr=args.addr) while True: diff --git a/selfdrive/debug/format_fingerprints.py b/selfdrive/debug/format_fingerprints.py index bd5822729a..2a5e4e6080 100755 --- a/selfdrive/debug/format_fingerprints.py +++ b/selfdrive/debug/format_fingerprints.py @@ -67,7 +67,7 @@ def format_brand_fw_versions(brand, extra_fw_versions: None | dict[str, dict[tup extra_fw_versions = extra_fw_versions or {} fingerprints_file = os.path.join(BASEDIR, f"selfdrive/car/{brand}/fingerprints.py") - with open(fingerprints_file, "r") as f: + with open(fingerprints_file) as f: comments = [line for line in f.readlines() if line.startswith("#") and "noqa" not in line] with open(fingerprints_file, "w") as f: diff --git a/selfdrive/debug/internal/measure_modeld_packet_drop.py b/selfdrive/debug/internal/measure_modeld_packet_drop.py index 6b7f7dbd13..9814942ce2 100755 --- a/selfdrive/debug/internal/measure_modeld_packet_drop.py +++ b/selfdrive/debug/internal/measure_modeld_packet_drop.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 import cereal.messaging as messaging -from typing import Optional if __name__ == "__main__": modeld_sock = messaging.sub_sock("modelV2") last_frame_id = None - start_t: Optional[int] = None + start_t: int | None = None frame_cnt = 0 dropped = 0 diff --git a/selfdrive/debug/live_cpu_and_temp.py b/selfdrive/debug/live_cpu_and_temp.py index 06f1be0b00..8549b92665 100755 --- a/selfdrive/debug/live_cpu_and_temp.py +++ b/selfdrive/debug/live_cpu_and_temp.py @@ -5,7 +5,6 @@ from collections import defaultdict from cereal.messaging import SubMaster from openpilot.common.numpy_fast import mean -from typing import Optional, Dict def cputime_total(ct): return ct.user + ct.nice + ct.system + ct.idle + ct.iowait + ct.irq + ct.softirq @@ -42,8 +41,8 @@ if __name__ == "__main__": total_times = [0.]*8 busy_times = [0.]*8 - prev_proclog: Optional[capnp._DynamicStructReader] = None - prev_proclog_t: Optional[int] = None + prev_proclog: capnp._DynamicStructReader | None = None + prev_proclog_t: int | None = None while True: sm.update() @@ -76,7 +75,7 @@ if __name__ == "__main__": print(f"CPU {100.0 * mean(cores):.2f}% - RAM: {last_mem:.2f}% - Temp {last_temp:.2f}C") if args.cpu and prev_proclog is not None and prev_proclog_t is not None: - procs: Dict[str, float] = defaultdict(float) + procs: dict[str, float] = defaultdict(float) dt = (sm.logMonoTime['procLog'] - prev_proclog_t) / 1e9 for proc in m.procs: try: diff --git a/selfdrive/debug/test_fw_query_on_routes.py b/selfdrive/debug/test_fw_query_on_routes.py index dd6243a44c..cc6fc2ae17 100755 --- a/selfdrive/debug/test_fw_query_on_routes.py +++ b/selfdrive/debug/test_fw_query_on_routes.py @@ -44,11 +44,15 @@ if __name__ == "__main__": dongles = [] for route in tqdm(routes): - dongle_id = SegmentRange(route).dongle_id + sr = SegmentRange(route) + dongle_id = sr.dongle_id if dongle_id in dongles: continue + if sr.slice == '' and sr.selector is None: + route += '/0' + lr = LogReader(route, default_mode=ReadMode.QLOG) try: diff --git a/selfdrive/locationd/calibrationd.py b/selfdrive/locationd/calibrationd.py index 06be9f031a..1456bf16f5 100755 --- a/selfdrive/locationd/calibrationd.py +++ b/selfdrive/locationd/calibrationd.py @@ -10,7 +10,7 @@ import gc import os import capnp import numpy as np -from typing import List, NoReturn, Optional +from typing import NoReturn from cereal import log import cereal.messaging as messaging @@ -89,7 +89,7 @@ class Calibrator: valid_blocks: int = 0, wide_from_device_euler_init: np.ndarray = WIDE_FROM_DEVICE_EULER_INIT, height_init: np.ndarray = HEIGHT_INIT, - smooth_from: Optional[np.ndarray] = None) -> None: + smooth_from: np.ndarray | None = None) -> None: if not np.isfinite(rpy_init).all(): self.rpy = RPY_INIT.copy() else: @@ -125,7 +125,7 @@ class Calibrator: self.old_rpy = smooth_from self.old_rpy_weight = 1.0 - def get_valid_idxs(self) -> List[int]: + def get_valid_idxs(self) -> list[int]: # exclude current block_idx from validity window before_current = list(range(self.block_idx)) after_current = list(range(min(self.valid_blocks, self.block_idx + 1), self.valid_blocks)) @@ -175,12 +175,12 @@ class Calibrator: else: return self.rpy - def handle_cam_odom(self, trans: List[float], - rot: List[float], - wide_from_device_euler: List[float], - trans_std: List[float], - road_transform_trans: List[float], - road_transform_trans_std: List[float]) -> Optional[np.ndarray]: + def handle_cam_odom(self, trans: list[float], + rot: list[float], + wide_from_device_euler: list[float], + trans_std: list[float], + road_transform_trans: list[float], + road_transform_trans_std: list[float]) -> np.ndarray | None: self.old_rpy_weight = max(0.0, self.old_rpy_weight - 1/SMOOTH_CYCLES) straight_and_fast = ((self.v_ego > MIN_SPEED_FILTER) and (trans[0] > MIN_SPEED_FILTER) and (abs(rot[2]) < MAX_YAW_RATE_FILTER)) @@ -260,7 +260,7 @@ def main() -> NoReturn: set_realtime_priority(1) pm = messaging.PubMaster(['liveCalibration']) - sm = messaging.SubMaster(['cameraOdometry', 'carState', 'carParams'], poll=['cameraOdometry']) + sm = messaging.SubMaster(['cameraOdometry', 'carState', 'carParams'], poll='cameraOdometry') calibrator = Calibrator(param_put=True) diff --git a/selfdrive/locationd/helpers.py b/selfdrive/locationd/helpers.py index 93e2929139..c273ba87b3 100644 --- a/selfdrive/locationd/helpers.py +++ b/selfdrive/locationd/helpers.py @@ -1,5 +1,5 @@ import numpy as np -from typing import List, Optional, Tuple, Any +from typing import Any from cereal import log @@ -12,7 +12,7 @@ class NPQueue: def __len__(self) -> int: return len(self.arr) - def append(self, pt: List[float]) -> None: + def append(self, pt: list[float]) -> None: if len(self.arr) < self.maxlen: self.arr = np.append(self.arr, [pt], axis=0) else: @@ -21,33 +21,33 @@ class NPQueue: class PointBuckets: - def __init__(self, x_bounds: List[Tuple[float, float]], min_points: List[float], min_points_total: int, points_per_bucket: int, rowsize: int) -> None: + def __init__(self, x_bounds: list[tuple[float, float]], min_points: list[float], min_points_total: int, points_per_bucket: int, rowsize: int) -> None: self.x_bounds = x_bounds self.buckets = {bounds: NPQueue(maxlen=points_per_bucket, rowsize=rowsize) for bounds in x_bounds} self.buckets_min_points = dict(zip(x_bounds, min_points, strict=True)) self.min_points_total = min_points_total - def bucket_lengths(self) -> List[int]: - return [len(v) for v in self.buckets.values()] - def __len__(self) -> int: - return sum(self.bucket_lengths()) + return sum([len(v) for v in self.buckets.values()]) def is_valid(self) -> bool: individual_buckets_valid = all(len(v) >= min_pts for v, min_pts in zip(self.buckets.values(), self.buckets_min_points.values(), strict=True)) total_points_valid = self.__len__() >= self.min_points_total return individual_buckets_valid and total_points_valid + def is_calculable(self) -> bool: + return all(len(v) > 0 for v in self.buckets.values()) + def add_point(self, x: float, y: float, bucket_val: float) -> None: raise NotImplementedError - def get_points(self, num_points: Optional[int] = None) -> Any: + def get_points(self, num_points: int | None = None) -> Any: points = np.vstack([x.arr for x in self.buckets.values()]) if num_points is None: return points return points[np.random.choice(np.arange(len(points)), min(len(points), num_points), replace=False)] - def load_points(self, points: List[List[float]]) -> None: + def load_points(self, points: list[list[float]]) -> None: for point in points: self.add_point(*point) diff --git a/selfdrive/locationd/models/car_kf.py b/selfdrive/locationd/models/car_kf.py index 9230cb48f0..1f3e447a19 100755 --- a/selfdrive/locationd/models/car_kf.py +++ b/selfdrive/locationd/models/car_kf.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import math import sys -from typing import Any, Dict +from typing import Any import numpy as np @@ -70,7 +70,7 @@ class CarKalman(KalmanFilter): ]) P_initial = Q.copy() - obs_noise: Dict[int, Any] = { + obs_noise: dict[int, Any] = { ObservationKind.STEER_ANGLE: np.atleast_2d(math.radians(0.05)**2), ObservationKind.ANGLE_OFFSET_FAST: np.atleast_2d(math.radians(10.0)**2), ObservationKind.ROAD_ROLL: np.atleast_2d(math.radians(1.0)**2), diff --git a/selfdrive/locationd/paramsd.py b/selfdrive/locationd/paramsd.py index 183a8666e8..d124eb5f05 100755 --- a/selfdrive/locationd/paramsd.py +++ b/selfdrive/locationd/paramsd.py @@ -124,7 +124,7 @@ def main(): REPLAY = bool(int(os.getenv("REPLAY", "0"))) pm = messaging.PubMaster(['liveParameters']) - sm = messaging.SubMaster(['liveLocationKalman', 'carState'], poll=['liveLocationKalman']) + sm = messaging.SubMaster(['liveLocationKalman', 'carState'], poll='liveLocationKalman') params_reader = Params() # wait for stats about the car to come in from controls diff --git a/selfdrive/locationd/torqued.py b/selfdrive/locationd/torqued.py index f2c0248afa..69bab8d1fa 100755 --- a/selfdrive/locationd/torqued.py +++ b/selfdrive/locationd/torqued.py @@ -184,23 +184,23 @@ class TorqueEstimator(ParameterEstimator): liveTorqueParameters.version = VERSION liveTorqueParameters.useParams = self.use_params - if self.filtered_points.is_valid(): + # Calculate raw estimates when possible, only update filters when enough points are gathered + if self.filtered_points.is_calculable(): latAccelFactor, latAccelOffset, frictionCoeff = self.estimate_params() liveTorqueParameters.latAccelFactorRaw = float(latAccelFactor) liveTorqueParameters.latAccelOffsetRaw = float(latAccelOffset) liveTorqueParameters.frictionCoefficientRaw = float(frictionCoeff) - if any(val is None or np.isnan(val) for val in [latAccelFactor, latAccelOffset, frictionCoeff]): - cloudlog.exception("Live torque parameters are invalid.") - liveTorqueParameters.liveValid = False - self.reset() - else: - liveTorqueParameters.liveValid = True - latAccelFactor = np.clip(latAccelFactor, self.min_lataccel_factor, self.max_lataccel_factor) - frictionCoeff = np.clip(frictionCoeff, self.min_friction, self.max_friction) - self.update_params({'latAccelFactor': latAccelFactor, 'latAccelOffset': latAccelOffset, 'frictionCoefficient': frictionCoeff}) - else: - liveTorqueParameters.liveValid = False + if self.filtered_points.is_valid(): + if any(val is None or np.isnan(val) for val in [latAccelFactor, latAccelOffset, frictionCoeff]): + cloudlog.exception("Live torque parameters are invalid.") + liveTorqueParameters.liveValid = False + self.reset() + else: + liveTorqueParameters.liveValid = True + latAccelFactor = np.clip(latAccelFactor, self.min_lataccel_factor, self.max_lataccel_factor) + frictionCoeff = np.clip(frictionCoeff, self.min_friction, self.max_friction) + self.update_params({'latAccelFactor': latAccelFactor, 'latAccelOffset': latAccelOffset, 'frictionCoefficient': frictionCoeff}) if with_points: liveTorqueParameters.points = self.filtered_points.get_points()[:, [0, 2]].tolist() @@ -218,7 +218,7 @@ def main(demo=False): config_realtime_process([0, 1, 2, 3], 5) pm = messaging.PubMaster(['liveTorqueParameters']) - sm = messaging.SubMaster(['carControl', 'carState', 'liveLocationKalman'], poll=['liveLocationKalman']) + sm = messaging.SubMaster(['carControl', 'carState', 'liveLocationKalman'], poll='liveLocationKalman') params = Params() with car.CarParams.from_bytes(params.get("CarParams", block=True)) as CP: diff --git a/selfdrive/manager/build.py b/selfdrive/manager/build.py index f2758cf32b..067e1b5a1e 100755 --- a/selfdrive/manager/build.py +++ b/selfdrive/manager/build.py @@ -2,7 +2,6 @@ import os import subprocess from pathlib import Path -from typing import List # NOTE: Do NOT import anything here that needs be built (e.g. params) from openpilot.common.basedir import BASEDIR @@ -29,7 +28,7 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None: # building with all cores can result in using too # much memory, so retry with less parallelism - compile_output: List[bytes] = [] + compile_output: list[bytes] = [] for n in (nproc, nproc/2, 1): compile_output.clear() scons: subprocess.Popen = subprocess.Popen(["scons", f"-j{int(n)}", "--cache-populate", *extra_args], cwd=BASEDIR, env=env, stderr=subprocess.PIPE) diff --git a/selfdrive/manager/helpers.py b/selfdrive/manager/helpers.py index 797f4ee92e..047d0ac2d6 100644 --- a/selfdrive/manager/helpers.py +++ b/selfdrive/manager/helpers.py @@ -1,9 +1,10 @@ +import errno +import fcntl import os import sys -import fcntl -import errno -import signal +import pathlib import shutil +import signal import subprocess import tempfile import threading @@ -52,7 +53,9 @@ def write_onroad_params(started, params): def save_bootlog(): # copy current params tmp = tempfile.mkdtemp() - shutil.copytree(Params().get_param_path() + "/..", tmp, dirs_exist_ok=True) + params_dirname = pathlib.Path(Params().get_param_path()).name + params_dir = os.path.join(tmp, params_dirname) + shutil.copytree(Params().get_param_path(), params_dir, dirs_exist_ok=True) def fn(tmpdir): env = os.environ.copy() diff --git a/selfdrive/manager/manager.py b/selfdrive/manager/manager.py index ddc1200cbc..24dceaaf08 100755 --- a/selfdrive/manager/manager.py +++ b/selfdrive/manager/manager.py @@ -4,7 +4,6 @@ import os import signal import sys import traceback -from typing import List, Tuple, Union from cereal import log import cereal.messaging as messaging @@ -19,7 +18,7 @@ from openpilot.selfdrive.athena.registration import register, UNREGISTERED_DONGL from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.system.version import is_dirty, get_commit, get_version, get_origin, get_short_branch, \ get_normalized_origin, terms_version, training_version, \ - is_tested_branch, is_release_branch + is_tested_branch, is_release_branch, get_commit_date @@ -33,7 +32,7 @@ def manager_init() -> None: if is_release_branch(): params.clear_all(ParamKeyType.DEVELOPMENT_ONLY) - default_params: List[Tuple[str, Union[str, bytes]]] = [ + default_params: list[tuple[str, str | bytes]] = [ ("CompletedTrainingVersion", "0"), ("DisengageOnAccelerator", "0"), ("GsmMetered", "1"), @@ -66,6 +65,7 @@ def manager_init() -> None: params.put("TermsVersion", terms_version) params.put("TrainingVersion", training_version) params.put("GitCommit", get_commit()) + params.put("GitCommitDate", get_commit_date()) params.put("GitBranch", get_short_branch()) params.put("GitRemote", get_origin()) params.put_bool("IsTestedBranch", is_tested_branch()) @@ -120,14 +120,14 @@ def manager_thread() -> None: params = Params() - ignore: List[str] = [] + ignore: list[str] = [] if params.get("DongleId", encoding='utf8') in (None, UNREGISTERED_DONGLE_ID): ignore += ["manage_athenad", "uploader"] if os.getenv("NOBOARD") is not None: ignore.append("pandad") ignore += [x for x in os.getenv("BLOCK", "").split(",") if len(x) > 0] - sm = messaging.SubMaster(['deviceState', 'carParams'], poll=['deviceState']) + sm = messaging.SubMaster(['deviceState', 'carParams'], poll='deviceState') pm = messaging.PubMaster(['managerState']) write_onroad_params(False, params) @@ -136,7 +136,7 @@ def manager_thread() -> None: started_prev = False while True: - sm.update() + sm.update(1000) started = sm['deviceState'].started @@ -153,7 +153,7 @@ def manager_thread() -> None: ensure_running(managed_processes.values(), started, params=params, CP=sm['carParams'], not_run=ignore) - running = ' '.join("%s%s\u001b[0m" % ("\u001b[32m" if p.proc.is_alive() else "\u001b[31m", p.name) + running = ' '.join("{}{}\u001b[0m".format("\u001b[32m" if p.proc.is_alive() else "\u001b[31m", p.name) for p in managed_processes.values() if p.proc) print(running) cloudlog.debug(running) diff --git a/selfdrive/manager/process.py b/selfdrive/manager/process.py index 523e1fd209..46fb68f89c 100644 --- a/selfdrive/manager/process.py +++ b/selfdrive/manager/process.py @@ -4,7 +4,7 @@ import signal import struct import time import subprocess -from typing import Optional, Callable, List, ValuesView +from collections.abc import Callable, ValuesView from abc import ABC, abstractmethod from multiprocessing import Process @@ -47,7 +47,7 @@ def launcher(proc: str, name: str) -> None: raise -def nativelauncher(pargs: List[str], cwd: str, name: str) -> None: +def nativelauncher(pargs: list[str], cwd: str, name: str) -> None: os.environ['MANAGER_DAEMON'] = name # exec the process @@ -67,12 +67,12 @@ class ManagerProcess(ABC): daemon = False sigkill = False should_run: Callable[[bool, Params, car.CarParams], bool] - proc: Optional[Process] = None + proc: Process | None = None enabled = True name = "" last_watchdog_time = 0 - watchdog_max_dt: Optional[int] = None + watchdog_max_dt: int | None = None watchdog_seen = False shutting_down = False @@ -109,7 +109,7 @@ class ManagerProcess(ABC): else: self.watchdog_seen = True - def stop(self, retry: bool = True, block: bool = True, sig: Optional[signal.Signals] = None) -> Optional[int]: + def stop(self, retry: bool = True, block: bool = True, sig: signal.Signals | None = None) -> int | None: if self.proc is None: return None @@ -274,18 +274,20 @@ class DaemonProcess(ManagerProcess): def ensure_running(procs: ValuesView[ManagerProcess], started: bool, params=None, CP: car.CarParams=None, - not_run: Optional[List[str]]=None) -> List[ManagerProcess]: + not_run: list[str] | None=None) -> list[ManagerProcess]: if not_run is None: not_run = [] running = [] for p in procs: if p.enabled and p.name not in not_run and p.should_run(started, params, CP): - p.start() running.append(p) else: p.stop(block=False) p.check_watchdog(started) + for p in running: + p.start() + return running diff --git a/selfdrive/manager/test/test_manager.py b/selfdrive/manager/test/test_manager.py index fbeb932a86..1ae94b26a1 100755 --- a/selfdrive/manager/test/test_manager.py +++ b/selfdrive/manager/test/test_manager.py @@ -5,6 +5,8 @@ import signal import time import unittest +from parameterized import parameterized + from cereal import car from openpilot.common.params import Params import openpilot.selfdrive.manager.manager as manager @@ -38,13 +40,13 @@ class TestManager(unittest.TestCase): # TODO: ensure there are blacklisted procs until we have a dedicated test self.assertTrue(len(BLACKLIST_PROCS), "No blacklisted procs to test not_run") - def test_startup_time(self): - for _ in range(10): - start = time.monotonic() - os.environ['PREPAREONLY'] = '1' - manager.main() - t = time.monotonic() - start - assert t < MAX_STARTUP_TIME, f"startup took {t}s, expected <{MAX_STARTUP_TIME}s" + @parameterized.expand([(i,) for i in range(10)]) + def test_startup_time(self, index): + start = time.monotonic() + os.environ['PREPAREONLY'] = '1' + manager.main() + t = time.monotonic() - start + assert t < MAX_STARTUP_TIME, f"startup took {t}s, expected <{MAX_STARTUP_TIME}s" @unittest.skip("this test is flaky the way it's currently written, should be moved to test_onroad") def test_clean_exit(self): diff --git a/selfdrive/modeld/dmonitoringmodeld.py b/selfdrive/modeld/dmonitoringmodeld.py index 1e25964702..ef403b44fc 100755 --- a/selfdrive/modeld/dmonitoringmodeld.py +++ b/selfdrive/modeld/dmonitoringmodeld.py @@ -6,7 +6,6 @@ import time import ctypes import numpy as np from pathlib import Path -from typing import Tuple, Dict from cereal import messaging from cereal.messaging import PubMaster, SubMaster @@ -53,7 +52,7 @@ class DMonitoringModelResult(ctypes.Structure): ("wheel_on_right_prob", ctypes.c_float)] class ModelState: - inputs: Dict[str, np.ndarray] + inputs: dict[str, np.ndarray] output: np.ndarray model: ModelRunner @@ -68,7 +67,7 @@ class ModelState: self.model.addInput("input_img", None) self.model.addInput("calib", self.inputs['calib']) - def run(self, buf:VisionBuf, calib:np.ndarray) -> Tuple[np.ndarray, float]: + def run(self, buf:VisionBuf, calib:np.ndarray) -> tuple[np.ndarray, float]: self.inputs['calib'][:] = calib v_offset = buf.height - MODEL_HEIGHT diff --git a/selfdrive/modeld/fill_model_msg.py b/selfdrive/modeld/fill_model_msg.py index 93d1c7e77b..c39ec2da3d 100644 --- a/selfdrive/modeld/fill_model_msg.py +++ b/selfdrive/modeld/fill_model_msg.py @@ -1,7 +1,6 @@ import os import capnp import numpy as np -from typing import Dict from cereal import log from openpilot.selfdrive.modeld.constants import ModelConstants, Plan, Meta @@ -42,10 +41,10 @@ def fill_xyvat(builder, t, x, y, v, a, x_std=None, y_std=None, v_std=None, a_std if a_std is not None: builder.aStd = a_std.tolist() -def fill_model_msg(msg: capnp._DynamicStructBuilder, net_output_data: Dict[str, np.ndarray], publish_state: PublishState, +def fill_model_msg(msg: capnp._DynamicStructBuilder, net_output_data: dict[str, np.ndarray], publish_state: PublishState, vipc_frame_id: int, vipc_frame_id_extra: int, frame_id: int, frame_drop: float, timestamp_eof: int, timestamp_llk: int, model_execution_time: float, - nav_enabled: bool, v_ego: float, steer_delay: float, valid: bool) -> None: + nav_enabled: bool, valid: bool) -> None: frame_age = frame_id - vipc_frame_id if frame_id > vipc_frame_id else 0 msg.valid = valid @@ -174,7 +173,7 @@ def fill_model_msg(msg: capnp._DynamicStructBuilder, net_output_data: Dict[str, if SEND_RAW_PRED: modelV2.rawPredictions = net_output_data['raw_pred'].tobytes() -def fill_pose_msg(msg: capnp._DynamicStructBuilder, net_output_data: Dict[str, np.ndarray], +def fill_pose_msg(msg: capnp._DynamicStructBuilder, net_output_data: dict[str, np.ndarray], vipc_frame_id: int, vipc_dropped_frames: int, timestamp_eof: int, live_calib_seen: bool) -> None: msg.valid = live_calib_seen & (vipc_dropped_frames < 1) cameraOdometry = msg.cameraOdometry diff --git a/selfdrive/modeld/get_model_metadata.py b/selfdrive/modeld/get_model_metadata.py index 187f83399b..144860204f 100755 --- a/selfdrive/modeld/get_model_metadata.py +++ b/selfdrive/modeld/get_model_metadata.py @@ -4,9 +4,8 @@ import pathlib import onnx import codecs import pickle -from typing import Tuple -def get_name_and_shape(value_info:onnx.ValueInfoProto) -> Tuple[str, Tuple[int,...]]: +def get_name_and_shape(value_info:onnx.ValueInfoProto) -> tuple[str, tuple[int,...]]: shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim]) name = value_info.name return name, shape diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index 5b227d08e9..e086b8aaf8 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -6,7 +6,6 @@ import numpy as np import cereal.messaging as messaging from cereal import car, log from pathlib import Path -from typing import Dict, Optional from setproctitle import setproctitle from cereal.messaging import PubMaster, SubMaster from cereal.visionipc import VisionIpcClient, VisionStreamType, VisionBuf @@ -45,7 +44,7 @@ class FrameMeta: class ModelState: frame: ModelFrame wide_frame: ModelFrame - inputs: Dict[str, np.ndarray] + inputs: dict[str, np.ndarray] output: np.ndarray prev_desire: np.ndarray # for tracking the rising edge of the pulse model: ModelRunner @@ -78,14 +77,14 @@ class ModelState: for k,v in self.inputs.items(): self.model.addInput(k, v) - def slice_outputs(self, model_outputs: np.ndarray) -> Dict[str, np.ndarray]: + def slice_outputs(self, model_outputs: np.ndarray) -> dict[str, np.ndarray]: parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in self.output_slices.items()} if SEND_RAW_PRED: parsed_model_outputs['raw_pred'] = model_outputs.copy() return parsed_model_outputs def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray, - inputs: Dict[str, np.ndarray], prepare_only: bool) -> Optional[Dict[str, np.ndarray]]: + inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: # Model decides when action is completed, so desire input is just a pulse triggered on rising edge inputs['desire'][0] = 0 self.inputs['desire'][:-ModelConstants.DESIRE_LEN] = self.inputs['desire'][ModelConstants.DESIRE_LEN:] @@ -116,12 +115,16 @@ class ModelState: def main(demo=False): + cloudlog.warning("modeld init") + sentry.set_tag("daemon", PROCESS_NAME) cloudlog.bind(daemon=PROCESS_NAME) setproctitle(PROCESS_NAME) config_realtime_process(7, 54) + cloudlog.warning("setting up CL context") cl_context = CLContext() + cloudlog.warning("CL context ready; loading model") model = ModelState(cl_context) cloudlog.warning("models loaded, modeld starting") @@ -152,12 +155,8 @@ def main(demo=False): pm = PubMaster(["modelV2", "cameraOdometry"]) sm = SubMaster(["carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "navModel", "navInstruction", "carControl"]) - publish_state = PublishState() params = Params() - with car.CarParams.from_bytes(params.get("CarParams", block=True)) as msg: - steer_delay = msg.steerActuatorDelay + .2 - #steer_delay = 0.4 # setup filter to track dropped frames frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_FREQ) @@ -177,13 +176,15 @@ def main(demo=False): if demo: CP = get_demo_car_params() - with car.CarParams.from_bytes(params.get("CarParams", block=True)) as msg: - CP = msg - cloudlog.info("plannerd got CarParams: %s", CP.carName) + else: + with car.CarParams.from_bytes(params.get("CarParams", block=True)) as msg: + CP = msg + cloudlog.info("modeld got CarParams: %s", CP.carName) + # TODO this needs more thought, use .2s extra for now to estimate other delays steer_delay = CP.steerActuatorDelay + .2 - DH = DesireHelper() + DH = DesireHelper() while True: # Keep receiving frames until we are at least 1 frame ahead of previous extra frame @@ -219,13 +220,10 @@ def main(demo=False): buf_extra = buf_main meta_extra = meta_main - # TODO: path planner timeout? sm.update(0) desire = DH.desire - v_ego = sm["carState"].vEgo is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId - # TODO add lag lateral_control_params = np.array([sm["carState"].vEgo, steer_delay], dtype=np.float32) if sm.updated["liveCalibration"]: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) @@ -277,7 +275,7 @@ def main(demo=False): if prepare_only: cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames") - inputs:Dict[str, np.ndarray] = { + inputs:dict[str, np.ndarray] = { 'desire': vec_desire, 'traffic_convention': traffic_convention, 'lateral_control_params': lateral_control_params, @@ -293,7 +291,7 @@ def main(demo=False): modelv2_send = messaging.new_message('modelV2') posenet_send = messaging.new_message('cameraOdometry') fill_model_msg(modelv2_send, model_output, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio, - meta_main.timestamp_eof, timestamp_llk, model_execution_time, nav_enabled, v_ego, steer_delay, live_calib_seen) + meta_main.timestamp_eof, timestamp_llk, model_execution_time, nav_enabled, live_calib_seen) desire_state = modelv2_send.modelV2.meta.desireState l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] diff --git a/selfdrive/modeld/models/commonmodel_pyx.pxd b/selfdrive/modeld/models/commonmodel_pyx.pxd index 21c0716de4..97e3914588 100644 --- a/selfdrive/modeld/models/commonmodel_pyx.pxd +++ b/selfdrive/modeld/models/commonmodel_pyx.pxd @@ -7,7 +7,7 @@ cdef class CLContext(BaseCLContext): pass cdef class CLMem: - cdef cl_mem * mem; + cdef cl_mem * mem @staticmethod cdef create(void*) diff --git a/selfdrive/modeld/models/supercombo.onnx b/selfdrive/modeld/models/supercombo.onnx index 1777949cdd..ce346a406a 100644 --- a/selfdrive/modeld/models/supercombo.onnx +++ b/selfdrive/modeld/models/supercombo.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:738ef0a3407e1f918d928dd195dc0e2a326f7610a38184c4801a0f717a8255ea -size 48219112 +oid sha256:c11f99aab832242a604b1bb4c4cf391c9c3d5e90cbc07ab0d4c133473b56a3a4 +size 48193749 diff --git a/selfdrive/modeld/navmodeld.py b/selfdrive/modeld/navmodeld.py index ed0b597dfe..4672734681 100755 --- a/selfdrive/modeld/navmodeld.py +++ b/selfdrive/modeld/navmodeld.py @@ -5,7 +5,6 @@ import time import ctypes import numpy as np from pathlib import Path -from typing import Tuple, Dict from cereal import messaging from cereal.messaging import PubMaster, SubMaster @@ -41,7 +40,7 @@ class NavModelResult(ctypes.Structure): ("features", ctypes.c_float*NAV_FEATURE_LEN)] class ModelState: - inputs: Dict[str, np.ndarray] + inputs: dict[str, np.ndarray] output: np.ndarray model: ModelRunner @@ -52,7 +51,7 @@ class ModelState: self.model = ModelRunner(MODEL_PATHS, self.output, Runtime.DSP, True, None) self.model.addInput("input_img", None) - def run(self, buf:np.ndarray) -> Tuple[np.ndarray, float]: + def run(self, buf:np.ndarray) -> tuple[np.ndarray, float]: self.inputs['input_img'][:] = buf t1 = time.perf_counter() diff --git a/selfdrive/modeld/parse_model_outputs.py b/selfdrive/modeld/parse_model_outputs.py index 01cba29d1c..af57e11d03 100644 --- a/selfdrive/modeld/parse_model_outputs.py +++ b/selfdrive/modeld/parse_model_outputs.py @@ -1,5 +1,4 @@ import numpy as np -from typing import Dict from openpilot.selfdrive.modeld.constants import ModelConstants def sigmoid(x): @@ -82,7 +81,7 @@ class Parser: outs[name] = pred_mu_final.reshape(final_shape) outs[name + '_stds'] = pred_std_final.reshape(final_shape) - def parse_outputs(self, outs: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]: + def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION, out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) diff --git a/selfdrive/modeld/runners/onnxmodel.py b/selfdrive/modeld/runners/onnxmodel.py index f86bee3878..69b44a5a97 100644 --- a/selfdrive/modeld/runners/onnxmodel.py +++ b/selfdrive/modeld/runners/onnxmodel.py @@ -3,7 +3,7 @@ import itertools import os import sys import numpy as np -from typing import Tuple, Dict, Union, Any +from typing import Any from openpilot.selfdrive.modeld.runners.runmodel_pyx import RunModel @@ -38,7 +38,7 @@ def create_ort_session(path, fp16_to_fp32): options = ort.SessionOptions() options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL - provider: Union[str, Tuple[str, Dict[Any, Any]]] + provider: str | tuple[str, dict[Any, Any]] if 'OpenVINOExecutionProvider' in ort.get_available_providers() and 'ONNXCPU' not in os.environ: provider = 'OpenVINOExecutionProvider' elif 'CUDAExecutionProvider' in ort.get_available_providers() and 'ONNXCPU' not in os.environ: diff --git a/selfdrive/modeld/runners/runmodel_pyx.pyx b/selfdrive/modeld/runners/runmodel_pyx.pyx index cdc62a79be..e1b201a6a9 100644 --- a/selfdrive/modeld/runners/runmodel_pyx.pyx +++ b/selfdrive/modeld/runners/runmodel_pyx.pyx @@ -2,7 +2,6 @@ # cython: c_string_encoding=ascii from libcpp.string cimport string -from libc.string cimport memcpy from .runmodel cimport USE_CPU_RUNTIME, USE_GPU_RUNTIME, USE_DSP_RUNTIME from selfdrive.modeld.models.commonmodel_pyx cimport CLMem diff --git a/selfdrive/modeld/thneed/serialize.cc b/selfdrive/modeld/thneed/serialize.cc index 6ed5c08e81..3dc2bef414 100644 --- a/selfdrive/modeld/thneed/serialize.cc +++ b/selfdrive/modeld/thneed/serialize.cc @@ -4,13 +4,14 @@ #include "third_party/json11/json11.hpp" #include "common/util.h" #include "common/clutil.h" +#include "common/swaglog.h" #include "selfdrive/modeld/thneed/thneed.h" using namespace json11; extern map g_program_source; void Thneed::load(const char *filename) { - printf("Thneed::load: loading from %s\n", filename); + LOGD("Thneed::load: loading from %s\n", filename); string buf = util::read_file(filename); int jsz = *(int *)buf.data(); @@ -74,8 +75,8 @@ void Thneed::load(const char *filename) { clbuf = clCreateImage(context, CL_MEM_READ_WRITE, &format, &desc, NULL, &errcode); #endif if (clbuf == NULL) { - printf("clError: %s create image %zux%zu rp %zu with buffer %p\n", cl_get_error_string(errcode), - desc.image_width, desc.image_height, desc.image_row_pitch, desc.buffer); + LOGE("clError: %s create image %zux%zu rp %zu with buffer %p\n", cl_get_error_string(errcode), + desc.image_width, desc.image_height, desc.image_row_pitch, desc.buffer); } assert(clbuf != NULL); } @@ -95,11 +96,11 @@ void Thneed::load(const char *filename) { cl_mem aa = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; input_clmem.push_back(aa); input_sizes.push_back(sz); - printf("Thneed::load: adding input %s with size %d\n", mobj["name"].string_value().data(), sz); + LOGD("Thneed::load: adding input %s with size %d\n", mobj["name"].string_value().data(), sz); cl_int cl_err; void *ret = clEnqueueMapBuffer(command_queue, aa, CL_TRUE, CL_MAP_WRITE, 0, sz, 0, NULL, NULL, &cl_err); - if (cl_err != CL_SUCCESS) printf("clError: %s map %p %d\n", cl_get_error_string(cl_err), aa, sz); + if (cl_err != CL_SUCCESS) LOGE("clError: %s map %p %d\n", cl_get_error_string(cl_err), aa, sz); assert(cl_err == CL_SUCCESS); inputs.push_back(ret); } @@ -107,7 +108,7 @@ void Thneed::load(const char *filename) { for (auto &obj : jdat["outputs"].array_items()) { auto mobj = obj.object_items(); int sz = mobj["size"].int_value(); - printf("Thneed::save: adding output with size %d\n", sz); + LOGD("Thneed::save: adding output with size %d\n", sz); // TODO: support multiple outputs output = real_mem[*(cl_mem*)(mobj["buffer_id"].string_value().data())]; assert(output != NULL); diff --git a/selfdrive/modeld/transforms/transform.cl b/selfdrive/modeld/transforms/transform.cl index 357ef87321..2ca25920cd 100644 --- a/selfdrive/modeld/transforms/transform.cl +++ b/selfdrive/modeld/transforms/transform.cl @@ -22,20 +22,20 @@ __kernel void warpPerspective(__global const uchar * src, W = W != 0.0f ? INTER_TAB_SIZE / W : 0.0f; int X = rint(X0 * W), Y = rint(Y0 * W); - short sx = convert_short_sat(X >> INTER_BITS); - short sy = convert_short_sat(Y >> INTER_BITS); + int sx = convert_short_sat(X >> INTER_BITS); + int sy = convert_short_sat(Y >> INTER_BITS); + + short sx_clamp = clamp(sx, 0, src_cols - 1); + short sx_p1_clamp = clamp(sx + 1, 0, src_cols - 1); + short sy_clamp = clamp(sy, 0, src_rows - 1); + short sy_p1_clamp = clamp(sy + 1, 0, src_rows - 1); + int v0 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); + int v1 = convert_int(src[mad24(sy_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); + int v2 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_clamp*src_px_stride)]); + int v3 = convert_int(src[mad24(sy_p1_clamp, src_row_stride, src_offset + sx_p1_clamp*src_px_stride)]); + short ay = (short)(Y & (INTER_TAB_SIZE - 1)); short ax = (short)(X & (INTER_TAB_SIZE - 1)); - - int v0 = (sx >= 0 && sx < src_cols && sy >= 0 && sy < src_rows) ? - convert_int(src[mad24(sy, src_row_stride, src_offset + sx*src_px_stride)]) : 0; - int v1 = (sx+1 >= 0 && sx+1 < src_cols && sy >= 0 && sy < src_rows) ? - convert_int(src[mad24(sy, src_row_stride, src_offset + (sx+1)*src_px_stride)]) : 0; - int v2 = (sx >= 0 && sx < src_cols && sy+1 >= 0 && sy+1 < src_rows) ? - convert_int(src[mad24(sy+1, src_row_stride, src_offset + sx*src_px_stride)]) : 0; - int v3 = (sx+1 >= 0 && sx+1 < src_cols && sy+1 >= 0 && sy+1 < src_rows) ? - convert_int(src[mad24(sy+1, src_row_stride, src_offset + (sx+1)*src_px_stride)]) : 0; - float taby = 1.f/INTER_TAB_SIZE*ay; float tabx = 1.f/INTER_TAB_SIZE*ax; diff --git a/selfdrive/monitoring/dmonitoringd.py b/selfdrive/monitoring/dmonitoringd.py index 7a24c0107e..579e79093f 100755 --- a/selfdrive/monitoring/dmonitoringd.py +++ b/selfdrive/monitoring/dmonitoringd.py @@ -15,7 +15,7 @@ def dmonitoringd_thread(): params = Params() pm = messaging.PubMaster(['driverMonitoringState']) - sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'controlsState', 'modelV2'], poll=['driverStateV2']) + sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'controlsState', 'modelV2'], poll='driverStateV2') driver_status = DriverStatus(rhd_saved=params.get_bool("IsRhdDetected")) @@ -43,7 +43,7 @@ def dmonitoringd_thread(): # Get data from dmonitoringmodeld events = Events() - if sm.all_checks(): + if sm.all_checks() and len(sm['liveCalibration'].rpyCalib): driver_status.update_states(sm['driverStateV2'], sm['liveCalibration'].rpyCalib, sm['carState'].vEgo, sm['controlsState'].enabled) # Block engaging after max number of distrations diff --git a/selfdrive/navd/helpers.py b/selfdrive/navd/helpers.py index 55c3f88a9a..5b0f5b7e85 100644 --- a/selfdrive/navd/helpers.py +++ b/selfdrive/navd/helpers.py @@ -2,7 +2,7 @@ from __future__ import annotations import json import math -from typing import Any, Dict, List, Optional, Tuple, Union, cast +from typing import Any, cast from openpilot.common.conversions import Conversions from openpilot.common.numpy_fast import clip @@ -22,13 +22,13 @@ class Coordinate: def __init__(self, latitude: float, longitude: float) -> None: self.latitude = latitude self.longitude = longitude - self.annotations: Dict[str, float] = {} + self.annotations: dict[str, float] = {} @classmethod - def from_mapbox_tuple(cls, t: Tuple[float, float]) -> Coordinate: + def from_mapbox_tuple(cls, t: tuple[float, float]) -> Coordinate: return cls(t[1], t[0]) - def as_dict(self) -> Dict[str, float]: + def as_dict(self) -> dict[str, float]: return {'latitude': self.latitude, 'longitude': self.longitude} def __str__(self) -> str: @@ -83,7 +83,7 @@ def minimum_distance(a: Coordinate, b: Coordinate, p: Coordinate): return projection.distance_to(p) -def distance_along_geometry(geometry: List[Coordinate], pos: Coordinate) -> float: +def distance_along_geometry(geometry: list[Coordinate], pos: Coordinate) -> float: if len(geometry) <= 2: return geometry[0].distance_to(pos) @@ -106,7 +106,7 @@ def distance_along_geometry(geometry: List[Coordinate], pos: Coordinate) -> floa return total_distance_closest -def coordinate_from_param(param: str, params: Optional[Params] = None) -> Optional[Coordinate]: +def coordinate_from_param(param: str, params: Params | None = None) -> Coordinate | None: if params is None: params = Params() @@ -130,7 +130,7 @@ def string_to_direction(direction: str) -> str: return 'none' -def maxspeed_to_ms(maxspeed: Dict[str, Union[str, float]]) -> float: +def maxspeed_to_ms(maxspeed: dict[str, str | float]) -> float: unit = cast(str, maxspeed['unit']) speed = cast(float, maxspeed['speed']) return SPEED_CONVERSIONS[unit] * speed @@ -140,7 +140,7 @@ def field_valid(dat: dict, field: str) -> bool: return field in dat and dat[field] is not None -def parse_banner_instructions(banners: Any, distance_to_maneuver: float = 0.0) -> Optional[Dict[str, Any]]: +def parse_banner_instructions(banners: Any, distance_to_maneuver: float = 0.0) -> dict[str, Any] | None: if not len(banners): return None diff --git a/selfdrive/navd/tests/test_navd.py b/selfdrive/navd/tests/test_navd.py index e2a5944c2b..61be6cc387 100755 --- a/selfdrive/navd/tests/test_navd.py +++ b/selfdrive/navd/tests/test_navd.py @@ -4,6 +4,8 @@ import random import unittest import numpy as np +from parameterized import parameterized + import cereal.messaging as messaging from openpilot.common.params import Params from openpilot.selfdrive.manager.process_config import managed_processes @@ -24,7 +26,7 @@ class TestNavd(unittest.TestCase): managed_processes['navd'].start() for _ in range(30): self.sm.update(1000) - if all(f > 0 for f in self.sm.rcv_frame.values()): + if all(f > 0 for f in self.sm.recv_frame.values()): break else: raise Exception("didn't get a route") @@ -50,11 +52,11 @@ class TestNavd(unittest.TestCase): } self._check_route(start, end) - def test_random(self): - for _ in range(10): - start = {"latitude": random.uniform(-90, 90), "longitude": random.uniform(-180, 180)} - end = {"latitude": random.uniform(-90, 90), "longitude": random.uniform(-180, 180)} - self._check_route(start, end, check_coords=False) + @parameterized.expand([(i,) for i in range(10)]) + def test_random(self, index): + start = {"latitude": random.uniform(-90, 90), "longitude": random.uniform(-180, 180)} + end = {"latitude": random.uniform(-90, 90), "longitude": random.uniform(-180, 180)} + self._check_route(start, end, check_coords=False) if __name__ == "__main__": diff --git a/selfdrive/statsd.py b/selfdrive/statsd.py index 94572b82c7..299aa295d7 100755 --- a/selfdrive/statsd.py +++ b/selfdrive/statsd.py @@ -5,7 +5,7 @@ import time from pathlib import Path from collections import defaultdict from datetime import datetime, timezone -from typing import NoReturn, Union, List, Dict +from typing import NoReturn from openpilot.common.params import Params from cereal.messaging import SubMaster @@ -61,7 +61,7 @@ class StatLog: def main() -> NoReturn: dongle_id = Params().get("DongleId", encoding='utf-8') - def get_influxdb_line(measurement: str, value: Union[float, Dict[str, float]], timestamp: datetime, tags: dict) -> str: + def get_influxdb_line(measurement: str, value: float | dict[str, float], timestamp: datetime, tags: dict) -> str: res = f"{measurement}" for k, v in tags.items(): res += f",{k}={str(v)}" @@ -102,7 +102,7 @@ def main() -> NoReturn: idx = 0 last_flush_time = time.monotonic() gauges = {} - samples: Dict[str, List[float]] = defaultdict(list) + samples: dict[str, list[float]] = defaultdict(list) try: while True: started_prev = sm['deviceState'].started diff --git a/selfdrive/test/ciui.py b/selfdrive/test/ciui.py index 3f33847b29..f3b0c1a98f 100755 --- a/selfdrive/test/ciui.py +++ b/selfdrive/test/ciui.py @@ -11,7 +11,7 @@ from openpilot.selfdrive.ui.qt.python_helpers import set_main_window class Window(QWidget): def __init__(self, parent=None): - super(Window, self).__init__(parent) + super().__init__(parent) layout = QVBoxLayout() self.setLayout(layout) @@ -47,7 +47,7 @@ class Window(QWidget): def update(self): for cmd, label in self.labels.items(): - out = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + out = subprocess.run(cmd, capture_output=True, shell=True, check=False, encoding='utf8').stdout label.setText(out.strip()) diff --git a/selfdrive/test/docker_common.sh b/selfdrive/test/docker_common.sh index 92da71ba66..f8a423762d 100644 --- a/selfdrive/test/docker_common.sh +++ b/selfdrive/test/docker_common.sh @@ -7,9 +7,6 @@ elif [ "$1" = "sim" ]; then elif [ "$1" = "prebuilt" ]; then export DOCKER_IMAGE=openpilot-prebuilt export DOCKER_FILE=Dockerfile.openpilot -elif [ "$1" = "cl" ]; then - export DOCKER_IMAGE=openpilot-base-cl - export DOCKER_FILE=Dockerfile.openpilot_base_cl else echo "Invalid docker build image: '$1'" exit 1 diff --git a/selfdrive/test/fuzzy_generation.py b/selfdrive/test/fuzzy_generation.py index 28c70a0ff4..00e98fadc1 100644 --- a/selfdrive/test/fuzzy_generation.py +++ b/selfdrive/test/fuzzy_generation.py @@ -1,6 +1,7 @@ import capnp import hypothesis.strategies as st -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any +from collections.abc import Callable from cereal import log @@ -12,7 +13,7 @@ class FuzzyGenerator: self.draw = draw self.real_floats = real_floats - def generate_native_type(self, field: str) -> st.SearchStrategy[Union[bool, int, float, str, bytes]]: + def generate_native_type(self, field: str) -> st.SearchStrategy[bool | int | float | str | bytes]: def floats(**kwargs) -> st.SearchStrategy[float]: allow_nan = not self.real_floats allow_infinity = not self.real_floats @@ -67,18 +68,18 @@ class FuzzyGenerator: else: return self.generate_struct(field.schema) - def generate_struct(self, schema: capnp.lib.capnp._StructSchema, event: Optional[str] = None) -> st.SearchStrategy[Dict[str, Any]]: - full_fill: List[str] = list(schema.non_union_fields) - single_fill: List[str] = [event] if event else [self.draw(st.sampled_from(schema.union_fields))] if schema.union_fields else [] + def generate_struct(self, schema: capnp.lib.capnp._StructSchema, event: str | None = None) -> st.SearchStrategy[dict[str, Any]]: + full_fill: list[str] = list(schema.non_union_fields) + single_fill: list[str] = [event] if event else [self.draw(st.sampled_from(schema.union_fields))] if schema.union_fields else [] return st.fixed_dictionaries({field: self.generate_field(schema.fields[field]) for field in full_fill + single_fill}) @classmethod - def get_random_msg(cls, draw: DrawType, struct: capnp.lib.capnp._StructModule, real_floats: bool = False) -> Dict[str, Any]: + def get_random_msg(cls, draw: DrawType, struct: capnp.lib.capnp._StructModule, real_floats: bool = False) -> dict[str, Any]: fg = cls(draw, real_floats=real_floats) - data: Dict[str, Any] = draw(fg.generate_struct(struct.schema)) + data: dict[str, Any] = draw(fg.generate_struct(struct.schema)) return data @classmethod - def get_random_event_msg(cls, draw: DrawType, events: List[str], real_floats: bool = False) -> List[Dict[str, Any]]: + def get_random_event_msg(cls, draw: DrawType, events: list[str], real_floats: bool = False) -> list[dict[str, Any]]: fg = cls(draw, real_floats=real_floats) return [draw(fg.generate_struct(log.Event.schema, e)) for e in sorted(events)] diff --git a/selfdrive/test/helpers.py b/selfdrive/test/helpers.py index a8b7ca0c4d..a62f7ede85 100644 --- a/selfdrive/test/helpers.py +++ b/selfdrive/test/helpers.py @@ -11,7 +11,6 @@ from openpilot.system.version import training_version, terms_version def set_params_enabled(): - os.environ['REPLAY'] = "1" os.environ['FINGERPRINT'] = "TOYOTA COROLLA TSS2 2019" os.environ['LOGPRINT'] = "debug" @@ -73,7 +72,7 @@ def noop(*args, **kwargs): def read_segment_list(segment_list_path): - with open(segment_list_path, "r") as f: + with open(segment_list_path) 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)] diff --git a/selfdrive/test/loop_until_fail.sh b/selfdrive/test/loop_until_fail.sh new file mode 100755 index 0000000000..b73009dba6 --- /dev/null +++ b/selfdrive/test/loop_until_fail.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -e + +# Loop something forever until it fails, for verifying new tests + +while true; do + $@ +done diff --git a/selfdrive/test/process_replay/capture.py b/selfdrive/test/process_replay/capture.py index 28206c6b91..90c279ef35 100644 --- a/selfdrive/test/process_replay/capture.py +++ b/selfdrive/test/process_replay/capture.py @@ -1,7 +1,7 @@ import os import sys -from typing import Tuple, no_type_check +from typing import no_type_check class FdRedirect: def __init__(self, file_prefix: str, fd: int): @@ -53,7 +53,7 @@ class ProcessOutputCapture: self.stdout_redirect.link() self.stderr_redirect.link() - def read_outerr(self) -> Tuple[str, str]: + def read_outerr(self) -> tuple[str, str]: out_str = self.stdout_redirect.read().decode() err_str = self.stderr_redirect.read().decode() return out_str, err_str diff --git a/selfdrive/test/process_replay/compare_logs.py b/selfdrive/test/process_replay/compare_logs.py index dbb7c223f5..673f3b484c 100755 --- a/selfdrive/test/process_replay/compare_logs.py +++ b/selfdrive/test/process_replay/compare_logs.py @@ -5,7 +5,6 @@ import capnp import numbers import dictdiffer from collections import Counter -from typing import Dict from openpilot.tools.lib.logreader import LogReader @@ -97,7 +96,7 @@ def format_process_diff(diff): diff_short += f" {diff}\n" diff_long += f"\t{diff}\n" else: - cnt: Dict[str, int] = {} + cnt: dict[str, int] = {} for d in diff: diff_long += f"\t{str(d)}\n" diff --git a/selfdrive/test/process_replay/model_replay_ref_commit b/selfdrive/test/process_replay/model_replay_ref_commit index 2d886684af..3b7b04df80 100644 --- a/selfdrive/test/process_replay/model_replay_ref_commit +++ b/selfdrive/test/process_replay/model_replay_ref_commit @@ -1 +1 @@ -06bdc69a0229bece7b60178c421feda329522585 +fd6421f7551573c549480f9d29bb0dee4678344d diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index a6b2771668..b760548fd7 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -8,7 +8,8 @@ import signal import platform from collections import OrderedDict from dataclasses import dataclass, field -from typing import Dict, List, Optional, Callable, Union, Any, Iterable, Tuple +from typing import Any +from collections.abc import Callable, Iterable from tqdm import tqdm import capnp @@ -36,9 +37,9 @@ FAKEDATA = os.path.join(PROC_REPLAY_DIR, "fakedata/") class DummySocket: def __init__(self): - self.data: List[bytes] = [] + self.data: list[bytes] = [] - def receive(self, non_blocking: bool = False) -> Optional[bytes]: + def receive(self, non_blocking: bool = False) -> bytes | None: if non_blocking: return None @@ -128,21 +129,21 @@ class ReplayContext: @dataclass class ProcessConfig: proc_name: str - pubs: List[str] - subs: List[str] - ignore: List[str] - config_callback: Optional[Callable] = None - init_callback: Optional[Callable] = None - should_recv_callback: Optional[Callable] = None - tolerance: Optional[float] = None + pubs: list[str] + subs: list[str] + ignore: list[str] + config_callback: Callable | None = None + init_callback: Callable | None = None + should_recv_callback: Callable | None = None + tolerance: float | None = None processing_time: float = 0.001 timeout: int = 30 simulation: bool = True - main_pub: Optional[str] = None + main_pub: str | None = None main_pub_drained: bool = True - vision_pubs: List[str] = field(default_factory=list) - ignore_alive_pubs: List[str] = field(default_factory=list) - unlocked_pubs: List[str] = field(default_factory=list) + vision_pubs: list[str] = field(default_factory=list) + ignore_alive_pubs: list[str] = field(default_factory=list) + unlocked_pubs: list[str] = field(default_factory=list) class ProcessContainer: @@ -150,25 +151,25 @@ class ProcessContainer: self.prefix = OpenpilotPrefix(clean_dirs_on_exit=False) self.cfg = copy.deepcopy(cfg) self.process = copy.deepcopy(managed_processes[cfg.proc_name]) - self.msg_queue: List[capnp._DynamicStructReader] = [] + self.msg_queue: list[capnp._DynamicStructReader] = [] self.cnt = 0 - self.pm: Optional[messaging.PubMaster] = None - self.sockets: Optional[List[messaging.SubSocket]] = None - self.rc: Optional[ReplayContext] = None - self.vipc_server: Optional[VisionIpcServer] = None - self.environ_config: Optional[Dict[str, Any]] = None - self.capture: Optional[ProcessOutputCapture] = None + self.pm: messaging.PubMaster | None = None + self.sockets: list[messaging.SubSocket] | None = None + self.rc: ReplayContext | None = None + self.vipc_server: VisionIpcServer | None = None + self.environ_config: dict[str, Any] | None = None + self.capture: ProcessOutputCapture | None = None @property def has_empty_queue(self) -> bool: return len(self.msg_queue) == 0 @property - def pubs(self) -> List[str]: + def pubs(self) -> list[str]: return self.cfg.pubs @property - def subs(self) -> List[str]: + def subs(self) -> list[str]: return self.cfg.subs def _clean_env(self): @@ -180,7 +181,7 @@ class ProcessContainer: if k in os.environ: del os.environ[k] - def _setup_env(self, params_config: Dict[str, Any], environ_config: Dict[str, Any]): + def _setup_env(self, params_config: dict[str, Any], environ_config: dict[str, Any]): for k, v in environ_config.items(): if len(v) != 0: os.environ[k] = v @@ -202,7 +203,7 @@ class ProcessContainer: self.environ_config = environ_config - def _setup_vision_ipc(self, all_msgs: LogIterable, frs: Dict[str, Any]): + def _setup_vision_ipc(self, all_msgs: LogIterable, frs: dict[str, Any]): assert len(self.cfg.vision_pubs) != 0 vipc_server = VisionIpcServer("camerad") @@ -223,9 +224,9 @@ class ProcessContainer: self.process.start() def start( - self, params_config: Dict[str, Any], environ_config: Dict[str, Any], - all_msgs: LogIterable, frs: Optional[Dict[str, BaseFrameReader]], - fingerprint: Optional[str], capture_output: bool + self, params_config: dict[str, Any], environ_config: dict[str, Any], + all_msgs: LogIterable, frs: dict[str, BaseFrameReader] | None, + fingerprint: str | None, capture_output: bool ): with self.prefix as p: self._setup_env(params_config, environ_config) @@ -266,7 +267,7 @@ class ProcessContainer: self.prefix.clean_dirs() self._clean_env() - def run_step(self, msg: capnp._DynamicStructReader, frs: Optional[Dict[str, BaseFrameReader]]) -> List[capnp._DynamicStructReader]: + def run_step(self, msg: capnp._DynamicStructReader, frs: dict[str, BaseFrameReader] | None) -> list[capnp._DynamicStructReader]: assert self.rc and self.pm and self.sockets and self.process.proc output_msgs = [] @@ -580,7 +581,7 @@ def get_process_config(name: str) -> ProcessConfig: raise Exception(f"Cannot find process config with name: {name}") from ex -def get_custom_params_from_lr(lr: LogIterable, initial_state: str = "first") -> Dict[str, Any]: +def get_custom_params_from_lr(lr: LogIterable, initial_state: str = "first") -> dict[str, Any]: """ Use this to get custom params dict based on provided logs. Useful when replaying following processes: calibrationd, paramsd, torqued @@ -614,7 +615,7 @@ def get_custom_params_from_lr(lr: LogIterable, initial_state: str = "first") -> return custom_params -def replay_process_with_name(name: Union[str, Iterable[str]], lr: LogIterable, *args, **kwargs) -> List[capnp._DynamicStructReader]: +def replay_process_with_name(name: str | Iterable[str], lr: LogIterable, *args, **kwargs) -> list[capnp._DynamicStructReader]: if isinstance(name, str): cfgs = [get_process_config(name)] elif isinstance(name, Iterable): @@ -626,10 +627,10 @@ def replay_process_with_name(name: Union[str, Iterable[str]], lr: LogIterable, * def replay_process( - cfg: Union[ProcessConfig, Iterable[ProcessConfig]], lr: LogIterable, frs: Optional[Dict[str, BaseFrameReader]] = None, - fingerprint: Optional[str] = None, return_all_logs: bool = False, custom_params: Optional[Dict[str, Any]] = None, - captured_output_store: Optional[Dict[str, Dict[str, str]]] = None, disable_progress: bool = False -) -> List[capnp._DynamicStructReader]: + cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, BaseFrameReader] | None = None, + fingerprint: str | None = None, return_all_logs: bool = False, custom_params: dict[str, Any] | None = None, + captured_output_store: dict[str, dict[str, str]] | None = None, disable_progress: bool = False +) -> list[capnp._DynamicStructReader]: if isinstance(cfg, Iterable): cfgs = list(cfg) else: @@ -654,9 +655,9 @@ def replay_process( def _replay_multi_process( - cfgs: List[ProcessConfig], lr: LogIterable, frs: Optional[Dict[str, BaseFrameReader]], fingerprint: Optional[str], - custom_params: Optional[Dict[str, Any]], captured_output_store: Optional[Dict[str, Dict[str, str]]], disable_progress: bool -) -> List[capnp._DynamicStructReader]: + cfgs: list[ProcessConfig], lr: LogIterable, frs: dict[str, BaseFrameReader] | None, fingerprint: str | None, + custom_params: dict[str, Any] | None, captured_output_store: dict[str, dict[str, str]] | None, disable_progress: bool +) -> list[capnp._DynamicStructReader]: if fingerprint is not None: params_config = generate_params_config(lr=lr, fingerprint=fingerprint, custom_params=custom_params) env_config = generate_environ_config(fingerprint=fingerprint) @@ -690,10 +691,10 @@ def _replay_multi_process( pub_msgs = [msg for msg in all_msgs if msg.which() in lr_pubs] # external queue for messages taken from logs; internal queue for messages generated by processes, which will be republished - external_pub_queue: List[capnp._DynamicStructReader] = pub_msgs.copy() - internal_pub_queue: List[capnp._DynamicStructReader] = [] + external_pub_queue: list[capnp._DynamicStructReader] = pub_msgs.copy() + internal_pub_queue: list[capnp._DynamicStructReader] = [] # heap for maintaining the order of messages generated by processes, where each element: (logMonoTime, index in internal_pub_queue) - internal_pub_index_heap: List[Tuple[int, int]] = [] + internal_pub_index_heap: list[tuple[int, int]] = [] pbar = tqdm(total=len(external_pub_queue), disable=disable_progress) while len(external_pub_queue) != 0 or (len(internal_pub_index_heap) != 0 and not all(c.has_empty_queue for c in containers)): @@ -723,7 +724,7 @@ def _replay_multi_process( return log_msgs -def generate_params_config(lr=None, CP=None, fingerprint=None, custom_params=None) -> Dict[str, Any]: +def generate_params_config(lr=None, CP=None, fingerprint=None, custom_params=None) -> dict[str, Any]: params_dict = { "OpenpilotEnabledToggle": True, "DisengageOnAccelerator": True, @@ -755,14 +756,13 @@ def generate_params_config(lr=None, CP=None, fingerprint=None, custom_params=Non return params_dict -def generate_environ_config(CP=None, fingerprint=None, log_dir=None) -> Dict[str, Any]: +def generate_environ_config(CP=None, fingerprint=None, log_dir=None) -> dict[str, Any]: environ_dict = {} if platform.system() != "Darwin": environ_dict["PARAMS_ROOT"] = "/dev/shm/params" if log_dir is not None: environ_dict["LOG_ROOT"] = log_dir - environ_dict["NO_RADAR_SLEEP"] = "1" environ_dict["REPLAY"] = "1" # Regen or python process diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index bc79d3be9e..fc38f87310 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -d9a3a0d4e806b49ec537233d30926bec70308485 \ No newline at end of file +d0cdea7eb15f3cac8a921f7ace3eaa6baebb4fd5 diff --git a/selfdrive/test/process_replay/regen.py b/selfdrive/test/process_replay/regen.py index 245b5b2709..3bb51d0b65 100755 --- a/selfdrive/test/process_replay/regen.py +++ b/selfdrive/test/process_replay/regen.py @@ -5,7 +5,8 @@ import time import capnp import numpy as np -from typing import Union, Iterable, Optional, List, Any, Dict, Tuple +from typing import Any +from collections.abc import Iterable from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, FAKEDATA, ProcessConfig, replay_process, get_process_config, \ check_openpilot_enabled, get_custom_params_from_lr @@ -40,9 +41,9 @@ class DummyFrameReader(BaseFrameReader): def regen_segment( - lr: LogIterable, frs: Optional[Dict[str, Any]] = None, + lr: LogIterable, frs: dict[str, Any] | None = None, processes: Iterable[ProcessConfig] = CONFIGS, disable_tqdm: bool = False -) -> List[capnp._DynamicStructReader]: +) -> list[capnp._DynamicStructReader]: all_msgs = sorted(lr, key=lambda m: m.logMonoTime) custom_params = get_custom_params_from_lr(all_msgs) @@ -57,7 +58,7 @@ def regen_segment( def setup_data_readers( route: str, sidx: int, use_route_meta: bool, needs_driver_cam: bool = True, needs_road_cam: bool = True, dummy_driver_cam: bool = False -) -> Tuple[LogReader, Dict[str, Any]]: +) -> tuple[LogReader, dict[str, Any]]: if use_route_meta: r = Route(route) lr = LogReader(r.log_paths()[sidx]) @@ -92,7 +93,7 @@ def setup_data_readers( def regen_and_save( - route: str, sidx: int, processes: Union[str, Iterable[str]] = "all", outdir: str = FAKEDATA, + route: str, sidx: int, processes: str | Iterable[str] = "all", outdir: str = FAKEDATA, upload: bool = False, use_route_meta: bool = False, disable_tqdm: bool = False, dummy_driver_cam: bool = False ) -> str: if not isinstance(processes, str) and not hasattr(processes, "__iter__"): diff --git a/selfdrive/test/process_replay/test_debayer.py b/selfdrive/test/process_replay/test_debayer.py index bea1b1fb00..edf2cbd469 100755 --- a/selfdrive/test/process_replay/test_debayer.py +++ b/selfdrive/test/process_replay/test_debayer.py @@ -30,7 +30,7 @@ def get_frame_fn(ref_commit, test_route, tici=True): def bzip_frames(frames): - data = bytes() + data = b'' for y, u, v in frames: data += y.tobytes() data += u.tobytes() diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 407a103232..2b917b0f61 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -5,7 +5,7 @@ import os import sys from collections import defaultdict from tqdm import tqdm -from typing import Any, DefaultDict, Dict +from typing import Any from openpilot.selfdrive.car.car_helpers import interface_names from openpilot.tools.lib.openpilotci import get_url, upload_file @@ -58,7 +58,7 @@ segments = [ ("VOLKSWAGEN", "regen8BDFE7307A0|2023-10-30--23-19-36--0"), ("MAZDA", "regen2E9F1A15FD5|2023-10-30--23-20-36--0"), ("FORD", "regen6D39E54606E|2023-10-30--23-20-54--0"), - ] +] # dashcamOnly makes don't need to be tested until a full port is done excluded_interfaces = ["mock", "tesla"] @@ -107,7 +107,9 @@ def test_process(cfg, lr, segment, ref_log_path, new_log_path, ignore_fields=Non # check to make sure openpilot is engaged in the route if cfg.proc_name == "controlsd": if not check_openpilot_enabled(log_msgs): - return f"Route did not enable at all or for long enough: {new_log_path}", log_msgs + # FIXME: these segments should work, but the replay enabling logic is too brittle + if segment not in ("regen6CA24BC3035|2023-10-30--23-14-28--0", "regen7D2D3F82D5B|2023-10-30--23-15-55--0"): + return f"Route did not enable at all or for long enough: {new_log_path}", log_msgs try: return compare_logs(ref_log_msgs, log_msgs, ignore_fields + cfg.ignore, ignore_msgs, cfg.tolerance), log_msgs @@ -170,11 +172,11 @@ if __name__ == "__main__": untested = (set(interface_names) - set(excluded_interfaces)) - {c.lower() for c in tested_cars} assert len(untested) == 0, f"Cars missing routes: {str(untested)}" - log_paths: DefaultDict[str, Dict[str, Dict[str, str]]] = defaultdict(lambda: defaultdict(dict)) + log_paths: defaultdict[str, dict[str, dict[str, str]]] = defaultdict(lambda: defaultdict(dict)) with concurrent.futures.ProcessPoolExecutor(max_workers=args.jobs) as pool: if not args.upload_only: download_segments = [seg for car, seg in segments if car in tested_cars] - log_data: Dict[str, LogReader] = {} + log_data: dict[str, LogReader] = {} p1 = pool.map(get_log_data, download_segments) for segment, lr in tqdm(p1, desc="Getting Logs", total=len(download_segments)): log_data[segment] = lr diff --git a/selfdrive/test/profiling/profiler.py b/selfdrive/test/profiling/profiler.py index 6d0cc204c5..6571825418 100755 --- a/selfdrive/test/profiling/profiler.py +++ b/selfdrive/test/profiling/profiler.py @@ -80,12 +80,10 @@ def profile(proc, func, car='toyota'): if __name__ == '__main__': from openpilot.selfdrive.controls.controlsd import main as controlsd_thread - from openpilot.selfdrive.controls.radard import radard_thread from openpilot.selfdrive.locationd.paramsd import main as paramsd_thread from openpilot.selfdrive.controls.plannerd import main as plannerd_thread procs = { - 'radard': radard_thread, 'controlsd': controlsd_thread, 'paramsd': paramsd_thread, 'plannerd': plannerd_thread, diff --git a/selfdrive/test/scons_build_test.sh b/selfdrive/test/scons_build_test.sh new file mode 100755 index 0000000000..a3b33f797a --- /dev/null +++ b/selfdrive/test/scons_build_test.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +SCRIPT_DIR=$(dirname "$0") +BASEDIR=$(realpath "$SCRIPT_DIR/../../") +cd $BASEDIR + +# tests that our build system's dependencies are configured properly, +# needs a machine with lots of cores +scons --clean +scons --no-cache --random -j$(nproc) \ No newline at end of file diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 036eacfa48..de8a4420b3 100755 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -29,7 +29,7 @@ from openpilot.tools.lib.logreader import LogReader # Baseline CPU usage by process PROCS = { - "selfdrive.controls.controlsd": 39.0, + "selfdrive.controls.controlsd": 41.0, "./loggerd": 14.0, "./encoderd": 17.0, "./camerad": 14.5, @@ -39,7 +39,7 @@ PROCS = { "./ui": 18.0, "selfdrive.locationd.paramsd": 9.0, "./sensord": 7.0, - "selfdrive.controls.radard": 4.5, + "selfdrive.controls.radard": 7.0, "selfdrive.modeld.modeld": 13.0, "selfdrive.modeld.dmonitoringmodeld": 8.0, "selfdrive.modeld.navmodeld": 1.0, @@ -57,7 +57,7 @@ PROCS = { "selfdrive.boardd.pandad": 0, "selfdrive.statsd": 0.4, "selfdrive.navd.navd": 0.4, - "system.loggerd.uploader": (0.5, 10.0), + "system.loggerd.uploader": (0.5, 15.0), "system.loggerd.deleter": 0.1, } @@ -114,6 +114,7 @@ class TestOnroad(unittest.TestCase): params = Params() params.remove("CurrentRoute") set_params_enabled() + os.environ['REPLAY'] = '1' os.environ['TESTING_CLOSET'] = '1' if os.path.exists(Paths.log_root()): shutil.rmtree(Paths.log_root()) @@ -126,7 +127,7 @@ class TestOnroad(unittest.TestCase): sm = messaging.SubMaster(['carState']) with Timeout(150, "controls didn't start"): - while sm.rcv_frame['carState'] < 0: + while sm.recv_frame['carState'] < 0: sm.update(1000) # make sure we get at least two full segments diff --git a/selfdrive/test/test_time_to_onroad.py b/selfdrive/test/test_time_to_onroad.py index aec49cb13a..a3f803e221 100755 --- a/selfdrive/test/test_time_to_onroad.py +++ b/selfdrive/test/test_time_to_onroad.py @@ -18,30 +18,40 @@ def test_time_to_onroad(): proc = subprocess.Popen(["python", manager_path]) start_time = time.monotonic() - sm = messaging.SubMaster(['controlsState', 'deviceState', 'onroadEvents']) + sm = messaging.SubMaster(['controlsState', 'deviceState', 'onroadEvents', 'sendcan']) try: - # wait for onroad - with Timeout(20, "timed out waiting to go onroad"): - while True: - sm.update(1000) - if sm['deviceState'].started: - break - time.sleep(1) + # wait for onroad. timeout assumes panda is up to date + with Timeout(10, "timed out waiting to go onroad"): + while not sm['deviceState'].started: + sm.update(100) # wait for engageability - with Timeout(10, "timed out waiting for engageable"): - while True: - sm.update(1000) - if sm['controlsState'].engageable: - break - time.sleep(1) + try: + with Timeout(10, "timed out waiting for engageable"): + sendcan_frame = None + while True: + sm.update(100) + + # sendcan is only sent once we're initialized + if sm.seen['controlsState'] and sendcan_frame is None: + sendcan_frame = sm.frame + + if sendcan_frame is not None and sm.recv_frame['sendcan'] > sendcan_frame: + sm.update(100) + assert sm['controlsState'].engageable, f"events: {sm['onroadEvents']}" + break + finally: + print(f"onroad events: {sm['onroadEvents']}") print(f"engageable after {time.monotonic() - start_time:.2f}s") - # once we're enageable, must be for the next few seconds - for _ in range(500): + # once we're enageable, must stay for the next few seconds + st = time.monotonic() + while (time.monotonic() - st) < 10.: sm.update(100) + assert sm.all_alive(), sm.alive assert sm['controlsState'].engageable, f"events: {sm['onroadEvents']}" + assert sm['controlsState'].cumLagMs < 10. finally: proc.terminate() - if proc.wait(60) is None: + if proc.wait(20) is None: proc.kill() diff --git a/selfdrive/test/update_ci_routes.py b/selfdrive/test/update_ci_routes.py index 5ab5042b2b..bdfefb78d1 100755 --- a/selfdrive/test/update_ci_routes.py +++ b/selfdrive/test/update_ci_routes.py @@ -3,7 +3,7 @@ import os import re import subprocess import sys -from typing import Iterable, List, Optional +from collections.abc import Iterable from tqdm import tqdm @@ -12,14 +12,14 @@ from openpilot.selfdrive.test.process_replay.test_processes import source_segmen from openpilot.tools.lib.azure_container import AzureContainer from openpilot.tools.lib.openpilotcontainers import DataCIContainer, DataProdContainer, OpenpilotCIContainer -SOURCES: List[AzureContainer] = [ +SOURCES: list[AzureContainer] = [ DataProdContainer, DataCIContainer ] DEST = OpenpilotCIContainer -def upload_route(path: str, exclude_patterns: Optional[Iterable[str]] = None) -> None: +def upload_route(path: str, exclude_patterns: Iterable[str] | None = None) -> None: if exclude_patterns is None: exclude_patterns = [r'dcamera\.hevc'] diff --git a/selfdrive/thermald/power_monitoring.py b/selfdrive/thermald/power_monitoring.py index 8802a82af4..073589edb7 100644 --- a/selfdrive/thermald/power_monitoring.py +++ b/selfdrive/thermald/power_monitoring.py @@ -1,6 +1,5 @@ import time import threading -from typing import Optional from openpilot.common.params import Params from openpilot.system.hardware import HARDWARE @@ -14,7 +13,6 @@ CAR_BATTERY_CAPACITY_uWh = 30e6 CAR_CHARGING_RATE_W = 45 VBATT_PAUSE_CHARGING = 11.8 # Lower limit on the LPF car battery voltage -VBATT_INSTANT_PAUSE_CHARGING = 7.0 # Lower limit on the instant car battery voltage measurements to avoid triggering on instant power loss MAX_TIME_OFFROAD_S = 30*3600 MIN_ON_TIME_S = 3600 DELAY_SHUTDOWN_TIME_S = 300 # Wait at least DELAY_SHUTDOWN_TIME_S seconds after offroad_time to shutdown. @@ -39,7 +37,7 @@ class PowerMonitoring: self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), int(car_battery_capacity_uWh)) # Calculation tick - def calculate(self, voltage: Optional[int], ignition: bool): + def calculate(self, voltage: int | None, ignition: bool): try: now = time.monotonic() @@ -109,7 +107,7 @@ class PowerMonitoring: return int(self.car_battery_capacity_uWh) # See if we need to shutdown - def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: Optional[float], started_seen: bool): + def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: float | None, started_seen: bool): if offroad_timestamp is None: return False @@ -117,7 +115,6 @@ class PowerMonitoring: should_shutdown = False offroad_time = (now - offroad_timestamp) low_voltage_shutdown = (self.car_voltage_mV < (VBATT_PAUSE_CHARGING * 1e3) and - self.car_voltage_instant_mV > (VBATT_INSTANT_PAUSE_CHARGING * 1e3) and offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S) should_shutdown |= offroad_time > MAX_TIME_OFFROAD_S should_shutdown |= low_voltage_shutdown diff --git a/selfdrive/thermald/thermald.py b/selfdrive/thermald/thermald.py index ab30b1579f..93ebd3ab87 100755 --- a/selfdrive/thermald/thermald.py +++ b/selfdrive/thermald/thermald.py @@ -6,7 +6,6 @@ import threading import time from collections import OrderedDict, namedtuple from pathlib import Path -from typing import Dict, Optional, Tuple import psutil @@ -50,9 +49,9 @@ THERMAL_BANDS = OrderedDict({ # Override to highest thermal band when offroad and above this temp OFFROAD_DANGER_TEMP = 75 -prev_offroad_states: Dict[str, Tuple[bool, Optional[str]]] = {} +prev_offroad_states: dict[str, tuple[bool, str | None]] = {} -tz_by_type: Optional[Dict[str, int]] = None +tz_by_type: dict[str, int] | None = None def populate_tz_by_type(): global tz_by_type tz_by_type = {} @@ -83,12 +82,11 @@ def read_thermal(thermal_config): dat.deviceState.cpuTempC = [read_tz(z) / thermal_config.cpu[1] for z in thermal_config.cpu[0]] dat.deviceState.gpuTempC = [read_tz(z) / thermal_config.gpu[1] for z in thermal_config.gpu[0]] dat.deviceState.memoryTempC = read_tz(thermal_config.mem[0]) / thermal_config.mem[1] - dat.deviceState.ambientTempC = read_tz(thermal_config.ambient[0]) / thermal_config.ambient[1] dat.deviceState.pmicTempC = [read_tz(z) / thermal_config.pmic[1] for z in thermal_config.pmic[0]] return dat -def set_offroad_alert_if_changed(offroad_alert: str, show_alert: bool, extra_text: Optional[str]=None): +def set_offroad_alert_if_changed(offroad_alert: str, show_alert: bool, extra_text: str | None=None): if prev_offroad_states.get(offroad_alert, None) == (show_alert, extra_text): return prev_offroad_states[offroad_alert] = (show_alert, extra_text) @@ -166,20 +164,20 @@ def hw_state_thread(end_event, hw_queue): def thermald_thread(end_event, hw_queue) -> None: pm = messaging.PubMaster(['deviceState']) - sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "controlsState", "pandaStates"], poll=["pandaStates"]) + sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "controlsState", "pandaStates"], poll="pandaStates") count = 0 - onroad_conditions: Dict[str, bool] = { + onroad_conditions: dict[str, bool] = { "ignition": False, } - startup_conditions: Dict[str, bool] = {} - startup_conditions_prev: Dict[str, bool] = {} + startup_conditions: dict[str, bool] = {} + startup_conditions_prev: dict[str, bool] = {} - off_ts: Optional[float] = None - started_ts: Optional[float] = None + off_ts: float | None = None + started_ts: float | None = None started_seen = False - startup_blocked_ts: Optional[float] = None + startup_blocked_ts: float | None = None thermal_status = ThermalStatus.yellow last_hw_state = HardwareState( @@ -233,7 +231,7 @@ def thermald_thread(end_event, hw_queue) -> None: if TICI: fan_controller = TiciFanController() - elif (time.monotonic() - sm.rcv_time['pandaStates']) > DISCONNECT_TIMEOUT: + elif (time.monotonic() - sm.recv_time['pandaStates']) > DISCONNECT_TIMEOUT: if onroad_conditions["ignition"]: onroad_conditions["ignition"] = False cloudlog.error("panda timed out onroad") @@ -245,8 +243,10 @@ def thermald_thread(end_event, hw_queue) -> None: msg.deviceState.freeSpacePercent = get_available_percent(default=100.0) msg.deviceState.memoryUsagePercent = int(round(psutil.virtual_memory().percent)) - msg.deviceState.cpuUsagePercent = [int(round(n)) for n in psutil.cpu_percent(percpu=True)] msg.deviceState.gpuUsagePercent = int(round(HARDWARE.get_gpu_usage_percent())) + online_cpu_usage = [int(round(n)) for n in psutil.cpu_percent(percpu=True)] + offline_cpu_usage = [0., ] * (len(msg.deviceState.cpuTempC) - len(online_cpu_usage)) + msg.deviceState.cpuUsagePercent = online_cpu_usage + offline_cpu_usage msg.deviceState.networkType = last_hw_state.network_type msg.deviceState.networkMetered = last_hw_state.network_metered @@ -410,7 +410,6 @@ def thermald_thread(end_event, hw_queue) -> None: for i, temp in enumerate(msg.deviceState.gpuTempC): statlog.gauge(f"gpu{i}_temperature", temp) statlog.gauge("memory_temperature", msg.deviceState.memoryTempC) - statlog.gauge("ambient_temperature", msg.deviceState.ambientTempC) for i, temp in enumerate(msg.deviceState.pmicTempC): statlog.gauge(f"pmic{i}_temperature", temp) for i, temp in enumerate(last_hw_state.nvme_temps): diff --git a/selfdrive/ui/.gitignore b/selfdrive/ui/.gitignore index 99f9097905..f9724816da 100644 --- a/selfdrive/ui/.gitignore +++ b/selfdrive/ui/.gitignore @@ -3,11 +3,13 @@ moc_* translations/main_test_en.* +_text +_spinner + ui +mui watch3 installer/installers/* -qt/text -qt/spinner qt/setup/setup qt/setup/reset qt/setup/wifi diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 4c866332f9..ea5734fe6e 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -72,8 +72,8 @@ asset_obj = qt_env.Object("assets", assets) qt_env.SharedLibrary("qt/python_helpers", ["qt/qt_window.cc"], LIBS=qt_libs) # spinner and text window -qt_env.Program("qt/text", ["qt/text.cc"], LIBS=qt_libs) -qt_env.Program("qt/spinner", ["qt/spinner.cc"], LIBS=qt_libs) +qt_env.Program("_text", ["qt/text.cc"], LIBS=qt_libs) +qt_env.Program("_spinner", ["qt/spinner.cc"], LIBS=qt_libs) # build main UI qt_env.Program("ui", qt_src + [asset_obj], LIBS=qt_libs) @@ -92,6 +92,9 @@ if GetOption('extras') and arch != "Darwin": # build updater UI qt_env.Program("qt/setup/updater", ["qt/setup/updater.cc", asset_obj], LIBS=qt_libs) + # build mui + qt_env.Program("mui", ["mui.cc"], LIBS=qt_libs) + # build installers senv = qt_env.Clone() senv['LINKFLAGS'].append('-Wl,-strip-debug') diff --git a/selfdrive/ui/mui.cc b/selfdrive/ui/mui.cc new file mode 100644 index 0000000000..98029ee311 --- /dev/null +++ b/selfdrive/ui/mui.cc @@ -0,0 +1,50 @@ +#include +#include +#include + +#include "cereal/messaging/messaging.h" +#include "selfdrive/ui/ui.h" +#include "selfdrive/ui/qt/qt_window.h" + +int main(int argc, char *argv[]) { + QApplication a(argc, argv); + QWidget w; + setMainWindow(&w); + + w.setStyleSheet("background-color: black;"); + + // our beautiful UI + QVBoxLayout *layout = new QVBoxLayout(&w); + QLabel *label = new QLabel("〇"); + layout->addWidget(label, 0, Qt::AlignCenter); + + QTimer timer; + QObject::connect(&timer, &QTimer::timeout, [=]() { + static SubMaster sm({"deviceState", "controlsState"}); + + bool onroad_prev = sm.allAliveAndValid({"deviceState"}) && + sm["deviceState"].getDeviceState().getStarted(); + sm.update(0); + + bool onroad = sm.allAliveAndValid({"deviceState"}) && + sm["deviceState"].getDeviceState().getStarted(); + + if (onroad) { + label->setText("〇"); + auto cs = sm["controlsState"].getControlsState(); + UIStatus status = cs.getEnabled() ? STATUS_ENGAGED : STATUS_DISENGAGED; + label->setStyleSheet(QString("color: %1; font-size: 250px;").arg(bg_colors[status].name())); + } else { + label->setText("offroad"); + label->setStyleSheet("color: grey; font-size: 40px;"); + } + + if ((onroad != onroad_prev) || sm.frame < 2) { + Hardware::set_brightness(50); + Hardware::set_display_power(onroad); + } + }); + timer.start(50); + + return a.exec(); +} diff --git a/selfdrive/ui/qt/offroad/onboarding.cc b/selfdrive/ui/qt/offroad/onboarding.cc index d4fcee55ce..b1219055fd 100644 --- a/selfdrive/ui/qt/offroad/onboarding.cc +++ b/selfdrive/ui/qt/offroad/onboarding.cc @@ -199,7 +199,7 @@ OnboardingWindow::OnboardingWindow(QWidget *parent) : QStackedWidget(parent) { TermsPage* terms = new TermsPage(this); addWidget(terms); connect(terms, &TermsPage::acceptedTerms, [=]() { - Params().put("HasAcceptedTerms", current_terms_version); + params.put("HasAcceptedTerms", current_terms_version); accepted_terms = true; updateActiveScreen(); }); @@ -209,7 +209,7 @@ OnboardingWindow::OnboardingWindow(QWidget *parent) : QStackedWidget(parent) { addWidget(tr); connect(tr, &TrainingGuide::completedTraining, [=]() { training_done = true; - Params().put("CompletedTrainingVersion", current_training_version); + params.put("CompletedTrainingVersion", current_training_version); updateActiveScreen(); }); diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index 4df0ed81cb..01750eaf2f 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -289,7 +289,7 @@ void AnnotatedCameraWidget::updateState(const UIState &s) { const auto nav_instruction = sm["navInstruction"].getNavInstruction(); // Handle older routes where vCruiseCluster is not set - float v_cruise = cs.getVCruiseCluster() == 0.0 ? cs.getVCruise() : cs.getVCruiseCluster(); + float v_cruise = cs.getVCruiseCluster() == 0.0 ? cs.getVCruise() : cs.getVCruiseCluster(); setSpeed = cs_alive ? v_cruise : SET_SPEED_NA; is_cruise_set = setSpeed > 0 && (int)setSpeed != SET_SPEED_NA; if (is_cruise_set && !s.scene.is_metric) { diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc index 16d31ce304..bc3c494fa7 100644 --- a/selfdrive/ui/qt/util.cc +++ b/selfdrive/ui/qt/util.cc @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -114,6 +115,11 @@ void initApp(int argc, char *argv[], bool disable_hidpi) { qputenv("QT_DBL_CLICK_DIST", QByteArray::number(150)); + // ensure the current dir matches the exectuable's directory + QApplication tmp(argc, argv); + QString appDir = QCoreApplication::applicationDirPath(); + QDir::setCurrent(appDir); + setQtSurfaceFormat(); } diff --git a/selfdrive/ui/soundd.py b/selfdrive/ui/soundd.py index 01148ec199..0550a7db9e 100644 --- a/selfdrive/ui/soundd.py +++ b/selfdrive/ui/soundd.py @@ -3,7 +3,6 @@ import numpy as np import time import wave -from typing import Dict, Optional, Tuple from cereal import car, messaging from openpilot.common.basedir import BASEDIR @@ -27,7 +26,7 @@ DB_SCALE = 30 # AMBIENT_DB + DB_SCALE is where MAX_VOLUME is applied AudibleAlert = car.CarControl.HUDControl.AudibleAlert -sound_list: Dict[int, Tuple[str, Optional[int], float]] = { +sound_list: dict[int, tuple[str, int | None, float]] = { # AudibleAlert, file name, play count (none for infinite) AudibleAlert.engage: ("engage.wav", 1, MAX_VOLUME), AudibleAlert.disengage: ("disengage.wav", 1, MAX_VOLUME), @@ -42,7 +41,7 @@ sound_list: Dict[int, Tuple[str, Optional[int], float]] = { } def check_controls_timeout_alert(sm): - controls_missing = time.monotonic() - sm.rcv_time['controlsState'] + controls_missing = time.monotonic() - sm.recv_time['controlsState'] if controls_missing > CONTROLS_TIMEOUT: if sm['controlsState'].enabled and (controls_missing - CONTROLS_TIMEOUT) < 10: @@ -64,7 +63,7 @@ class Soundd: self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False) def load_sounds(self): - self.loaded_sounds: Dict[int, np.ndarray] = {} + self.loaded_sounds: dict[int, np.ndarray] = {} # Load all sounds for sound in sound_list: diff --git a/selfdrive/ui/spinner b/selfdrive/ui/spinner index 35feab3f7e..965c8f581a 100755 --- a/selfdrive/ui/spinner +++ b/selfdrive/ui/spinner @@ -1,7 +1,7 @@ #!/bin/sh -if [ -f /TICI ] && [ ! -f qt/spinner ]; then - cp qt/spinner_larch64 qt/spinner +if [ -f /TICI ] && [ ! -f _spinner ]; then + cp qt/spinner_larch64 _spinner fi -exec ./qt/spinner "$1" +exec ./_spinner "$1" diff --git a/selfdrive/ui/tests/test_translations.py b/selfdrive/ui/tests/test_translations.py index 9ba9054ea1..8e50695e70 100755 --- a/selfdrive/ui/tests/test_translations.py +++ b/selfdrive/ui/tests/test_translations.py @@ -12,7 +12,7 @@ from parameterized import parameterized_class from openpilot.selfdrive.ui.update_translations import TRANSLATIONS_DIR, LANGUAGES_FILE, update_translations -with open(LANGUAGES_FILE, "r") as f: +with open(LANGUAGES_FILE) as f: translation_files = json.load(f) UNFINISHED_TRANSLATION_TAG = " None: t = datetime.datetime.utcnow() params.put(param, t.isoformat().encode('utf8')) -def read_time_from_param(params, param) -> Optional[datetime.datetime]: +def read_time_from_param(params, param) -> datetime.datetime | None: t = params.get(param, encoding='utf8') try: return datetime.datetime.fromisoformat(t) @@ -72,7 +71,7 @@ def read_time_from_param(params, param) -> Optional[datetime.datetime]: pass return None -def run(cmd: List[str], cwd: Optional[str] = None) -> str: +def run(cmd: list[str], cwd: str | None = None) -> str: return subprocess.check_output(cmd, cwd=cwd, stderr=subprocess.STDOUT, encoding='utf8') @@ -234,7 +233,7 @@ def handle_agnos_update() -> None: class Updater: def __init__(self): self.params = Params() - self.branches = defaultdict(lambda: '') + self.branches = defaultdict(str) self._has_internet: bool = False @property @@ -243,7 +242,7 @@ class Updater: @property def target_branch(self) -> str: - b: Union[str, None] = self.params.get("UpdaterTargetBranch", encoding='utf-8') + b: str | None = self.params.get("UpdaterTargetBranch", encoding='utf-8') if b is None: b = self.get_branch(BASEDIR) return b @@ -272,7 +271,7 @@ class Updater: def get_commit_hash(self, path: str = OVERLAY_MERGED) -> str: return run(["git", "rev-parse", "HEAD"], path).rstrip() - 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: str | None) -> None: self.params.put("UpdateFailedCount", str(failed_count)) self.params.put("UpdaterTargetBranch", self.target_branch) diff --git a/system/camerad/cameras/camera_qcom2.cc b/system/camerad/cameras/camera_qcom2.cc index 4ee167a4c3..619ce67c46 100644 --- a/system/camerad/cameras/camera_qcom2.cc +++ b/system/camerad/cameras/camera_qcom2.cc @@ -606,6 +606,7 @@ void CameraState::camera_open(MultiCameraState *multi_cam_state_, int camera_num LOGD("start csiphy: %d", ret); ret = device_control(multi_cam_state->isp_fd, CAM_START_DEV, session_handle, isp_dev_handle); LOGD("start isp: %d", ret); + assert(ret == 0); // TODO: this is unneeded, should we be doing the start i2c in a different way? //ret = device_control(sensor_fd, CAM_START_DEV, session_handle, sensor_dev_handle); diff --git a/system/hardware/base.py b/system/hardware/base.py index 9c7a618337..6cdb4a4d64 100644 --- a/system/hardware/base.py +++ b/system/hardware/base.py @@ -1,16 +1,15 @@ from abc import abstractmethod, ABC from collections import namedtuple -from typing import Dict from cereal import log -ThermalConfig = namedtuple('ThermalConfig', ['cpu', 'gpu', 'mem', 'bat', 'ambient', 'pmic']) +ThermalConfig = namedtuple('ThermalConfig', ['cpu', 'gpu', 'mem', 'bat', 'pmic']) NetworkType = log.DeviceState.NetworkType class HardwareBase(ABC): @staticmethod - def get_cmdline() -> Dict[str, str]: + def get_cmdline() -> dict[str, str]: with open('/proc/cmdline') as f: cmdline = f.read() return {kv[0]: kv[1] for kv in [s.split('=') for s in cmdline.split(' ')] if len(kv) == 2} diff --git a/system/hardware/pc/hardware.py b/system/hardware/pc/hardware.py index 4c2c104f94..719e272aea 100644 --- a/system/hardware/pc/hardware.py +++ b/system/hardware/pc/hardware.py @@ -57,7 +57,7 @@ class Pc(HardwareBase): print("SHUTDOWN!") def get_thermal_config(self): - return ThermalConfig(cpu=((None,), 1), gpu=((None,), 1), mem=(None, 1), bat=(None, 1), ambient=(None, 1), pmic=((None,), 1)) + return ThermalConfig(cpu=((None,), 1), gpu=((None,), 1), mem=(None, 1), bat=(None, 1), pmic=((None,), 1)) def set_screen_brightness(self, percentage): pass diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index a87f3d278d..e69842cfec 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -1,9 +1,9 @@ [ { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-1cc21f31a7c09772fd759e6f2a614974bf4f2fc320c91a799ffadd11abc1f85f.img.xz", - "hash": "1cc21f31a7c09772fd759e6f2a614974bf4f2fc320c91a799ffadd11abc1f85f", - "hash_raw": "1cc21f31a7c09772fd759e6f2a614974bf4f2fc320c91a799ffadd11abc1f85f", + "url": "https://commadist.azureedge.net/agnosupdate/boot-f0de74e139b8b99224738d4e72a5b1831758f20b09ff6bb28f3aaaae1c4c1ebe.img.xz", + "hash": "f0de74e139b8b99224738d4e72a5b1831758f20b09ff6bb28f3aaaae1c4c1ebe", + "hash_raw": "f0de74e139b8b99224738d4e72a5b1831758f20b09ff6bb28f3aaaae1c4c1ebe", "size": 15636480, "sparse": false, "full_check": true, @@ -61,17 +61,17 @@ }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-38402b90b65729f8a4feb729c8a862cdf306659a85f27431d3ff7e52d4027082.img.xz", - "hash": "5dc1718e21c49e4fa910fbb3b2321381f497b38335a0cf3ca923157d589abe89", - "hash_raw": "38402b90b65729f8a4feb729c8a862cdf306659a85f27431d3ff7e52d4027082", + "url": "https://commadist.azureedge.net/agnosupdate/system-0f69173d5f3058f7197c139442a6556be59e52f15402a263215a329ba5ec41e2.img.xz", + "hash": "4858385ba6284bcaa179ab77ac4263486e4d8670df921e4ac400464dc1dde59c", + "hash_raw": "0f69173d5f3058f7197c139442a6556be59e52f15402a263215a329ba5ec41e2", "size": 10737418240, "sparse": true, "full_check": false, "has_ab": true, "alt": { - "hash": "1809e36d8e376e0a0c8348e3f684aba4100fe0382042c051efd0e946af1ce696", - "url": "https://commadist.azureedge.net/agnosupdate/system-skip-chunks-38402b90b65729f8a4feb729c8a862cdf306659a85f27431d3ff7e52d4027082.img.xz", - "size": 4077270244 + "hash": "42658a6fff660d9b6abb9cb9fbb3481071259c9a9598718af6b1edff2b556009", + "url": "https://commadist.azureedge.net/agnosupdate/system-skip-chunks-0f69173d5f3058f7197c139442a6556be59e52f15402a263215a329ba5ec41e2.img.xz", + "size": 4548292756 } } ] \ No newline at end of file diff --git a/system/hardware/tici/agnos.py b/system/hardware/tici/agnos.py index ef7d9adb79..502295be07 100755 --- a/system/hardware/tici/agnos.py +++ b/system/hardware/tici/agnos.py @@ -6,7 +6,7 @@ import os import struct import subprocess import time -from typing import Dict, Generator, List, Tuple, Union +from collections.abc import Generator import requests @@ -20,7 +20,7 @@ class StreamingDecompressor: def __init__(self, url: str) -> None: self.buf = b"" - self.req = requests.get(url, stream=True, headers={'Accept-Encoding': None}, timeout=60) # type: ignore + self.req = requests.get(url, stream=True, headers={'Accept-Encoding': None}, timeout=60) self.it = self.req.iter_content(chunk_size=1024 * 1024) self.decompressor = lzma.LZMADecompressor(format=lzma.FORMAT_AUTO) self.eof = False @@ -117,7 +117,7 @@ def get_raw_hash(path: str, partition_size: int) -> str: return raw_hash.hexdigest().lower() -def verify_partition(target_slot_number: int, partition: Dict[str, Union[str, int]], force_full_check: bool = False) -> bool: +def verify_partition(target_slot_number: int, partition: dict[str, str | int], force_full_check: bool = False) -> bool: full_check = partition['full_check'] or force_full_check path = get_partition_path(target_slot_number, partition) @@ -184,7 +184,7 @@ def extract_casync_image(target_slot_number: int, partition: dict, cloudlog): target = casync.parse_caibx(partition['casync_caibx']) - sources: List[Tuple[str, casync.ChunkReader, casync.ChunkDict]] = [] + sources: list[tuple[str, casync.ChunkReader, casync.ChunkDict]] = [] # First source is the current partition. try: diff --git a/system/hardware/tici/amplifier.py b/system/hardware/tici/amplifier.py index e003f131cc..af82067467 100755 --- a/system/hardware/tici/amplifier.py +++ b/system/hardware/tici/amplifier.py @@ -2,7 +2,6 @@ import time from smbus2 import SMBus from collections import namedtuple -from typing import List # https://datasheets.maximintegrated.com/en/ds/MAX98089.pdf @@ -110,7 +109,7 @@ class Amplifier: def _get_shutdown_config(self, amp_disabled: bool) -> AmpConfig: return AmpConfig("Global shutdown", 0b0 if amp_disabled else 0b1, 0x51, 7, 0b10000000) - def _set_configs(self, configs: List[AmpConfig]) -> None: + def _set_configs(self, configs: list[AmpConfig]) -> None: with SMBus(self.AMP_I2C_BUS) as bus: for config in configs: if self.debug: @@ -123,7 +122,7 @@ class Amplifier: if self.debug: print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}") - def set_configs(self, configs: List[AmpConfig]) -> bool: + def set_configs(self, configs: list[AmpConfig]) -> bool: # retry in case panda is using the amp tries = 15 for i in range(15): diff --git a/system/hardware/tici/casync.py b/system/hardware/tici/casync.py index 993336616d..68ca37d38d 100755 --- a/system/hardware/tici/casync.py +++ b/system/hardware/tici/casync.py @@ -7,7 +7,7 @@ import sys import time from abc import ABC, abstractmethod from collections import defaultdict, namedtuple -from typing import Callable, Dict, List, Optional, Tuple +from collections.abc import Callable import requests from Crypto.Hash import SHA512 @@ -28,7 +28,7 @@ CHUNK_DOWNLOAD_RETRIES = 3 CAIBX_DOWNLOAD_TIMEOUT = 120 Chunk = namedtuple('Chunk', ['sha', 'offset', 'length']) -ChunkDict = Dict[bytes, Chunk] +ChunkDict = dict[bytes, Chunk] class ChunkReader(ABC): @@ -83,7 +83,7 @@ class RemoteChunkReader(ChunkReader): return decompressor.decompress(contents) -def parse_caibx(caibx_path: str) -> List[Chunk]: +def parse_caibx(caibx_path: str) -> list[Chunk]: """Parses the chunks from a caibx file. Can handle both local and remote files. Returns a list of chunks with hash, offset and length""" caibx: io.BufferedIOBase @@ -132,7 +132,7 @@ def parse_caibx(caibx_path: str) -> List[Chunk]: return chunks -def build_chunk_dict(chunks: List[Chunk]) -> ChunkDict: +def build_chunk_dict(chunks: list[Chunk]) -> ChunkDict: """Turn a list of chunks into a dict for faster lookups based on hash. Keep first chunk since it's more likely to be already downloaded.""" r = {} @@ -142,11 +142,11 @@ def build_chunk_dict(chunks: List[Chunk]) -> ChunkDict: return r -def extract(target: List[Chunk], - sources: List[Tuple[str, ChunkReader, ChunkDict]], +def extract(target: list[Chunk], + sources: list[tuple[str, ChunkReader, ChunkDict]], out_path: str, - progress: Optional[Callable[[int], None]] = None): - stats: Dict[str, int] = defaultdict(int) + progress: Callable[[int], None] | None = None): + stats: dict[str, int] = defaultdict(int) mode = 'rb+' if os.path.exists(out_path) else 'wb' with open(out_path, mode) as out: @@ -181,7 +181,7 @@ def extract(target: List[Chunk], return stats -def print_stats(stats: Dict[str, int]): +def print_stats(stats: dict[str, int]): total_bytes = sum(stats.values()) print(f"Total size: {total_bytes / 1024 / 1024:.2f} MB") for name, total in stats.items(): diff --git a/system/hardware/tici/hardware.h b/system/hardware/tici/hardware.h index f6ea86b002..e553a665a8 100644 --- a/system/hardware/tici/hardware.h +++ b/system/hardware/tici/hardware.h @@ -72,6 +72,7 @@ public: std::map ret = { {"/BUILD", util::read_file("/BUILD")}, {"lsblk", util::check_output("lsblk -o NAME,SIZE,STATE,VENDOR,MODEL,REV,SERIAL")}, + {"SOM ID", util::read_file("/sys/devices/platform/vendor/vendor:gpio-som-id/som_id")}, }; std::string bs = util::check_output("abctl --boot_slot"); diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index f606422028..5bb1032ba9 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -349,7 +349,6 @@ class Tici(HardwareBase): gpu=(("gpu0-usr", "gpu1-usr"), 1000), mem=("ddr-usr", 1000), bat=(None, 1), - ambient=("xo-therm-adc", 1000), pmic=(("pm8998_tz", "pm8005_tz"), 1000)) def set_screen_brightness(self, percentage): diff --git a/system/hardware/tici/power_monitor.py b/system/hardware/tici/power_monitor.py index ef3055ac47..296290dae8 100755 --- a/system/hardware/tici/power_monitor.py +++ b/system/hardware/tici/power_monitor.py @@ -3,7 +3,6 @@ import sys import time import datetime import numpy as np -from typing import List from collections import deque from openpilot.common.realtime import Ratekeeper @@ -14,7 +13,7 @@ def read_power(): with open("/sys/bus/i2c/devices/0-0040/hwmon/hwmon1/power1_input") as f: return int(f.read()) / 1e6 -def sample_power(seconds=5) -> List[float]: +def sample_power(seconds=5) -> list[float]: rate = 123 rk = Ratekeeper(rate, print_delay_threshold=None) diff --git a/system/hardware/tici/precise_power_measure.py b/system/hardware/tici/precise_power_measure.py index e186ba4aea..52fe0850ab 100755 --- a/system/hardware/tici/precise_power_measure.py +++ b/system/hardware/tici/precise_power_measure.py @@ -6,4 +6,4 @@ if __name__ == '__main__': print("measuring for 5 seconds") for _ in range(3): pwrs = sample_power() - print("mean %.2f std %.2f" % (np.mean(pwrs), np.std(pwrs))) + print(f"mean {np.mean(pwrs):.2f} std {np.std(pwrs):.2f}") diff --git a/system/hardware/tici/tests/compare_casync_manifest.py b/system/hardware/tici/tests/compare_casync_manifest.py index 835985b0b5..7de66d91d0 100755 --- a/system/hardware/tici/tests/compare_casync_manifest.py +++ b/system/hardware/tici/tests/compare_casync_manifest.py @@ -3,7 +3,6 @@ import argparse import collections import multiprocessing import os -from typing import Dict, List import requests from tqdm import tqdm @@ -42,7 +41,7 @@ if __name__ == "__main__": szs = list(tqdm(pool.imap(get_chunk_download_size, to), total=len(to))) chunk_sizes = {t.sha: sz for (t, sz) in zip(to, szs, strict=True)} - sources: Dict[str, List[int]] = { + sources: dict[str, list[int]] = { 'seed': [], 'remote_uncompressed': [], 'remote_compressed': [], diff --git a/system/hardware/tici/tests/test_power_draw.py b/system/hardware/tici/tests/test_power_draw.py index db75f79af5..352fcdf18a 100755 --- a/system/hardware/tici/tests/test_power_draw.py +++ b/system/hardware/tici/tests/test_power_draw.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 +from collections import defaultdict, deque +import sys import pytest import unittest import time import numpy as np from dataclasses import dataclass from tabulate import tabulate -from typing import List import cereal.messaging as messaging from cereal.services import SERVICE_LIST @@ -15,24 +16,28 @@ from openpilot.system.hardware.tici.power_monitor import get_power from openpilot.selfdrive.manager.process_config import managed_processes from openpilot.selfdrive.manager.manager import manager_cleanup -SAMPLE_TIME = 8 # seconds to sample power +SAMPLE_TIME = 8 # seconds to sample power +MAX_WARMUP_TIME = 30 # seconds to wait for SAMPLE_TIME consecutive valid samples @dataclass class Proc: - name: str + procs: list[str] power: float - msgs: List[str] + msgs: list[str] rtol: float = 0.05 atol: float = 0.12 - warmup: float = 6. + + @property + def name(self): + return '+'.join(self.procs) + PROCS = [ - Proc('camerad', 2.1, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']), - Proc('modeld', 1.12, atol=0.2, msgs=['modelV2']), - Proc('dmonitoringmodeld', 0.4, msgs=['driverStateV2']), - Proc('encoderd', 0.23, msgs=[]), - Proc('mapsd', 0.05, msgs=['mapRenderState']), - Proc('navmodeld', 0.05, msgs=['navModel']), + Proc(['camerad'], 2.1, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']), + Proc(['modeld'], 1.12, atol=0.2, msgs=['modelV2']), + Proc(['dmonitoringmodeld'], 0.4, msgs=['driverStateV2']), + Proc(['encoderd'], 0.23, msgs=[]), + Proc(['mapsd', 'navmodeld'], 0.05, msgs=['mapRenderState', 'navModel']), ] @@ -48,41 +53,83 @@ class TestPowerDraw(unittest.TestCase): def tearDown(self): manager_cleanup() + def get_expected_messages(self, proc): + return int(sum(SAMPLE_TIME * SERVICE_LIST[msg].frequency for msg in proc.msgs)) + + def valid_msg_count(self, proc, msg_counts): + msgs_received = sum(msg_counts[msg] for msg in proc.msgs) + msgs_expected = self.get_expected_messages(proc) + return np.core.numeric.isclose(msgs_expected, msgs_received, rtol=.02, atol=2) + + def valid_power_draw(self, proc, used): + return np.core.numeric.isclose(used, proc.power, rtol=proc.rtol, atol=proc.atol) + + def tabulate_msg_counts(self, msgs_and_power): + msg_counts = defaultdict(int) + for _, counts in msgs_and_power: + for msg, count in counts.items(): + msg_counts[msg] += count + return msg_counts + + def get_power_with_warmup_for_target(self, proc, prev): + socks = {msg: messaging.sub_sock(msg) for msg in proc.msgs} + for sock in socks.values(): + messaging.drain_sock_raw(sock) + + msgs_and_power = deque([], maxlen=SAMPLE_TIME) + + start_time = time.monotonic() + + while (time.monotonic() - start_time) < MAX_WARMUP_TIME: + power = get_power(1) + iteration_msg_counts = {} + for msg,sock in socks.items(): + iteration_msg_counts[msg] = len(messaging.drain_sock_raw(sock)) + msgs_and_power.append((power, iteration_msg_counts)) + + if len(msgs_and_power) < SAMPLE_TIME: + continue + + msg_counts = self.tabulate_msg_counts(msgs_and_power) + now = np.mean([m[0] for m in msgs_and_power]) + + if self.valid_msg_count(proc, msg_counts) and self.valid_power_draw(proc, now - prev): + break + + return now, msg_counts, time.monotonic() - start_time - SAMPLE_TIME + @mock_messages(['liveLocationKalman']) def test_camera_procs(self): baseline = get_power() prev = baseline used = {} + warmup_time = {} msg_counts = {} + for proc in PROCS: - socks = {msg: messaging.sub_sock(msg) for msg in proc.msgs} - managed_processes[proc.name].start() - time.sleep(proc.warmup) - for sock in socks.values(): - messaging.drain_sock_raw(sock) + for p in proc.procs: + managed_processes[p].start() + now, local_msg_counts, warmup_time[proc.name] = self.get_power_with_warmup_for_target(proc, prev) + msg_counts.update(local_msg_counts) - now = get_power(SAMPLE_TIME) used[proc.name] = now - prev prev = now - for msg,sock in socks.items(): - msg_counts[msg] = len(messaging.drain_sock_raw(sock)) manager_cleanup() - tab = [['process', 'expected (W)', 'measured (W)', '# msgs expected', '# msgs received']] + tab = [['process', 'expected (W)', 'measured (W)', '# msgs expected', '# msgs received', "warmup time (s)"]] for proc in PROCS: cur = used[proc.name] expected = proc.power msgs_received = sum(msg_counts[msg] for msg in proc.msgs) - msgs_expected = int(sum(SAMPLE_TIME * SERVICE_LIST[msg].frequency for msg in proc.msgs)) - tab.append([proc.name, round(expected, 2), round(cur, 2), msgs_expected, msgs_received]) + tab.append([proc.name, round(expected, 2), round(cur, 2), self.get_expected_messages(proc), msgs_received, round(warmup_time[proc.name], 2)]) with self.subTest(proc=proc.name): - np.testing.assert_allclose(msgs_expected, msgs_received, rtol=.02, atol=2) - np.testing.assert_allclose(cur, expected, rtol=proc.rtol, atol=proc.atol) + self.assertTrue(self.valid_msg_count(proc, msg_counts), f"expected {self.get_expected_messages(proc)} msgs, got {msgs_received} msgs") + self.assertTrue(self.valid_power_draw(proc, cur), f"expected {expected:.2f}W, got {cur:.2f}W") print(tabulate(tab)) print(f"Baseline {baseline:.2f}W\n") if __name__ == "__main__": - pytest.main() + pytest.main(sys.argv) diff --git a/system/loggerd/deleter.py b/system/loggerd/deleter.py index 868340150a..2f0b96c90e 100755 --- a/system/loggerd/deleter.py +++ b/system/loggerd/deleter.py @@ -2,7 +2,6 @@ import os import shutil import threading -from typing import List from openpilot.system.hardware.hw import Paths from openpilot.common.swaglog import cloudlog from openpilot.system.loggerd.config import get_available_bytes, get_available_percent @@ -23,7 +22,7 @@ def has_preserve_xattr(d: str) -> bool: return getxattr(os.path.join(Paths.log_root(), d), PRESERVE_ATTR_NAME) == PRESERVE_ATTR_VALUE -def get_preserved_segments(dirs_by_creation: List[str]) -> List[str]: +def get_preserved_segments(dirs_by_creation: list[str]) -> list[str]: preserved = [] for n, d in enumerate(filter(has_preserve_xattr, reversed(dirs_by_creation))): if n == PRESERVE_COUNT: diff --git a/system/loggerd/logger.cc b/system/loggerd/logger.cc index 2fc6492ad4..7a829a2f1f 100644 --- a/system/loggerd/logger.cc +++ b/system/loggerd/logger.cc @@ -44,6 +44,7 @@ kj::Array logger_build_init_data() { std::map params_map = params.readAll(); init.setGitCommit(params_map["GitCommit"]); + init.setGitCommitDate(params_map["GitCommitDate"]); init.setGitBranch(params_map["GitBranch"]); init.setGitRemote(params_map["GitRemote"]); init.setPassive(false); diff --git a/system/loggerd/tests/loggerd_tests_common.py b/system/loggerd/tests/loggerd_tests_common.py index 3aa9e40531..0532fe1a89 100644 --- a/system/loggerd/tests/loggerd_tests_common.py +++ b/system/loggerd/tests/loggerd_tests_common.py @@ -2,7 +2,6 @@ import os import random import unittest from pathlib import Path -from typing import Optional import openpilot.system.loggerd.deleter as deleter @@ -12,7 +11,7 @@ from openpilot.system.hardware.hw import Paths from openpilot.system.loggerd.xattr_cache import setxattr -def create_random_file(file_path: Path, size_mb: float, lock: bool = False, upload_xattr: Optional[bytes] = None) -> None: +def create_random_file(file_path: Path, size_mb: float, lock: bool = False, upload_xattr: bytes | None = None) -> None: file_path.parent.mkdir(parents=True, exist_ok=True) if lock: @@ -82,7 +81,7 @@ class UploaderTestCase(unittest.TestCase): self.params.put("DongleId", "0000000000000000") def make_file_with_data(self, f_dir: str, fn: str, size_mb: float = .1, lock: bool = False, - upload_xattr: Optional[bytes] = None, preserve_xattr: Optional[bytes] = None) -> Path: + upload_xattr: bytes | None = None, preserve_xattr: bytes | None = None) -> Path: file_path = Path(Paths.log_root()) / f_dir / fn create_random_file(file_path, size_mb, lock, upload_xattr) diff --git a/system/loggerd/tests/test_deleter.py b/system/loggerd/tests/test_deleter.py index e4112b7b4e..37d25507e0 100755 --- a/system/loggerd/tests/test_deleter.py +++ b/system/loggerd/tests/test_deleter.py @@ -4,7 +4,7 @@ import threading import unittest from collections import namedtuple from pathlib import Path -from typing import Sequence +from collections.abc import Sequence import openpilot.system.loggerd.deleter as deleter from openpilot.common.timeout import Timeout, TimeoutException diff --git a/system/loggerd/tests/test_loggerd.py b/system/loggerd/tests/test_loggerd.py index 0cd8548809..c80dc19fce 100755 --- a/system/loggerd/tests/test_loggerd.py +++ b/system/loggerd/tests/test_loggerd.py @@ -8,7 +8,6 @@ import subprocess import time from collections import defaultdict from pathlib import Path -from typing import Dict, List from flaky import flaky import cereal.messaging as messaging @@ -76,7 +75,7 @@ class TestLoggerd: end_type = SentinelType.endOfRoute if route else SentinelType.endOfSegment assert msgs[-1].sentinel.type == end_type - def _publish_random_messages(self, services: List[str]) -> Dict[str, list]: + def _publish_random_messages(self, services: list[str]) -> dict[str, list]: pm = messaging.PubMaster(services) managed_processes["loggerd"].start() @@ -107,6 +106,7 @@ class TestLoggerd: # param, initData field, value ("DongleId", "dongleId", dongle), ("GitCommit", "gitCommit", "commit"), + ("GitCommitDate", "gitCommitDate", "date"), ("GitBranch", "gitBranch", "branch"), ("GitRemote", "gitRemote", "remote"), ] diff --git a/system/loggerd/tests/test_uploader.py b/system/loggerd/tests/test_uploader.py index b674de5438..b807bd6b98 100755 --- a/system/loggerd/tests/test_uploader.py +++ b/system/loggerd/tests/test_uploader.py @@ -6,7 +6,6 @@ import unittest import logging import json from pathlib import Path -from typing import List, Optional from openpilot.system.hardware.hw import Paths from openpilot.common.swaglog import cloudlog @@ -53,7 +52,7 @@ class TestUploader(UploaderTestCase): self.end_event.set() self.up_thread.join() - def gen_files(self, lock=False, xattr: Optional[bytes] = None, boot=True) -> List[Path]: + def gen_files(self, lock=False, xattr: bytes | None = None, boot=True) -> list[Path]: f_paths = [] for t in ["qlog", "rlog", "dcamera.hevc", "fcamera.hevc"]: f_paths.append(self.make_file_with_data(self.seg_dir, t, 1, lock=lock, upload_xattr=xattr)) @@ -62,7 +61,7 @@ class TestUploader(UploaderTestCase): f_paths.append(self.make_file_with_data("boot", f"{self.seg_dir}", 1, lock=lock, upload_xattr=xattr)) return f_paths - def gen_order(self, seg1: List[int], seg2: List[int], boot=True) -> List[str]: + def gen_order(self, seg1: list[int], seg2: list[int], boot=True) -> list[str]: keys = [] if boot: keys += [f"boot/{self.seg_format.format(i)}.bz2" for i in seg1] diff --git a/system/loggerd/uploader.py b/system/loggerd/uploader.py index 105e830a4c..5ccf0ff69a 100755 --- a/system/loggerd/uploader.py +++ b/system/loggerd/uploader.py @@ -9,7 +9,8 @@ import threading import time import traceback import datetime -from typing import BinaryIO, Iterator, List, Optional, Tuple +from typing import BinaryIO +from collections.abc import Iterator from cereal import log import cereal.messaging as messaging @@ -42,10 +43,10 @@ class FakeResponse: self.request = FakeRequest() -def get_directory_sort(d: str) -> List[str]: +def get_directory_sort(d: str) -> list[str]: return [s.rjust(10, '0') for s in d.rsplit('--', 1)] -def listdir_by_creation(d: str) -> List[str]: +def listdir_by_creation(d: str) -> list[str]: if not os.path.isdir(d): return [] @@ -82,7 +83,7 @@ class Uploader: self.immediate_folders = ["crash/", "boot/"] self.immediate_priority = {"qlog": 0, "qlog.bz2": 0, "qcamera.ts": 1} - def list_upload_files(self, metered: bool) -> Iterator[Tuple[str, str, str]]: + def list_upload_files(self, metered: bool) -> Iterator[tuple[str, str, str]]: r = self.params.get("AthenadRecentlyViewedRoutes", encoding="utf8") requested_routes = [] if r is None else r.split(",") @@ -121,7 +122,7 @@ class Uploader: yield name, key, fn - def next_file_to_upload(self, metered: bool) -> Optional[Tuple[str, str, str]]: + def next_file_to_upload(self, metered: bool) -> tuple[str, str, str] | None: upload_files = list(self.list_upload_files(metered)) for name, key, fn in upload_files: @@ -207,10 +208,10 @@ class Uploader: return success - def step(self, network_type: int, metered: bool) -> bool: + def step(self, network_type: int, metered: bool) -> bool | None: d = self.next_file_to_upload(metered) if d is None: - return True + return None name, key, fn = d @@ -221,7 +222,7 @@ class Uploader: return self.upload(name, key, fn, network_type, metered) -def main(exit_event: Optional[threading.Event] = None) -> None: +def main(exit_event: threading.Event | None = None) -> None: if exit_event is None: exit_event = threading.Event() @@ -253,12 +254,15 @@ def main(exit_event: Optional[threading.Event] = None) -> None: continue success = uploader.step(sm['deviceState'].networkType.raw, sm['deviceState'].networkMetered) - if success: + if success is None: + backoff = 60 if offroad else 5 + elif success: backoff = 0.1 - elif allow_sleep: + else: cloudlog.info("upload backoff %r", backoff) backoff = min(backoff*2, 120) - time.sleep(backoff + random.uniform(0, backoff)) + if allow_sleep: + time.sleep(backoff + random.uniform(0, backoff)) if __name__ == "__main__": diff --git a/system/loggerd/xattr_cache.py b/system/loggerd/xattr_cache.py index 5feeff34d2..d3220118ac 100644 --- a/system/loggerd/xattr_cache.py +++ b/system/loggerd/xattr_cache.py @@ -1,10 +1,9 @@ import os import errno -from typing import Dict, Optional, Tuple -_cached_attributes: Dict[Tuple, Optional[bytes]] = {} +_cached_attributes: dict[tuple, bytes | None] = {} -def getxattr(path: str, attr_name: str) -> Optional[bytes]: +def getxattr(path: str, attr_name: str) -> bytes | None: key = (path, attr_name) if key not in _cached_attributes: try: diff --git a/system/qcomgpsd/nmeaport.py b/system/qcomgpsd/nmeaport.py index 231096fc5d..caff7af646 100644 --- a/system/qcomgpsd/nmeaport.py +++ b/system/qcomgpsd/nmeaport.py @@ -93,7 +93,7 @@ def nmea_checksum_ok(s): def process_nmea_port_messages(device:str="/dev/ttyUSB1") -> NoReturn: while True: try: - with open(device, "r") as nmeaport: + with open(device) as nmeaport: for line in nmeaport: line = line.strip() if DEBUG: diff --git a/system/qcomgpsd/qcomgpsd.py b/system/qcomgpsd/qcomgpsd.py index 3f72350234..e8c407a627 100755 --- a/system/qcomgpsd/qcomgpsd.py +++ b/system/qcomgpsd/qcomgpsd.py @@ -10,7 +10,7 @@ import shutil import subprocess import datetime from multiprocessing import Process, Event -from typing import NoReturn, Optional +from typing import NoReturn from struct import unpack_from, calcsize, pack from cereal import log @@ -90,15 +90,11 @@ def try_setup_logs(diag, logs): return setup_logs(diag, logs) @retry(attempts=3, delay=1.0) -def at_cmd(cmd: str) -> Optional[str]: +def at_cmd(cmd: str) -> str | None: return subprocess.check_output(f"mmcli -m any --timeout 30 --command='{cmd}'", shell=True, encoding='utf8') def gps_enabled() -> bool: - try: - p = subprocess.check_output("mmcli -m any --command=\"AT+QGPS?\"", shell=True) - return b"QGPS: 1" in p - except subprocess.CalledProcessError as exc: - raise Exception("failed to execute QGPS mmcli command") from exc + return "QGPS: 1" in at_cmd("AT+QGPS?") def download_assistance(): try: @@ -346,7 +342,7 @@ def main() -> NoReturn: gps.bearingDeg = report["q_FltHeadingRad"] * 180/math.pi # TODO needs update if there is another leap second, after june 2024? - dt_timestamp = (datetime.datetime(1980, 1, 6, 0, 0, 0, 0, datetime.timezone.utc) + + dt_timestamp = (datetime.datetime(1980, 1, 6, 0, 0, 0, 0, datetime.UTC) + datetime.timedelta(weeks=report['w_GpsWeekNumber']) + datetime.timedelta(seconds=(1e-3*report['q_GpsFixTimeMs'] - 18))) gps.unixTimestampMillis = dt_timestamp.timestamp()*1e3 diff --git a/system/qcomgpsd/tests/test_qcomgpsd.py b/system/qcomgpsd/tests/test_qcomgpsd.py index 8291f2cc32..6c93f7dd93 100755 --- a/system/qcomgpsd/tests/test_qcomgpsd.py +++ b/system/qcomgpsd/tests/test_qcomgpsd.py @@ -68,13 +68,14 @@ class TestRawgpsd(unittest.TestCase): def test_turns_off_gnss(self): for s in (0.1, 1, 5): - managed_processes['qcomgpsd'].start() - time.sleep(s) - managed_processes['qcomgpsd'].stop() + with self.subTest(runtime=s): + managed_processes['qcomgpsd'].start() + time.sleep(s) + managed_processes['qcomgpsd'].stop() - ls = subprocess.check_output("mmcli -m any --location-status --output-json", shell=True, encoding='utf-8') - loc_status = json.loads(ls) - assert set(loc_status['modem']['location']['enabled']) <= {'3gpp-lac-ci'} + ls = subprocess.check_output("mmcli -m any --location-status --output-json", shell=True, encoding='utf-8') + loc_status = json.loads(ls) + assert set(loc_status['modem']['location']['enabled']) <= {'3gpp-lac-ci'} def check_assistance(self, should_be_loaded): diff --git a/system/sensord/pigeond.py b/system/sensord/pigeond.py index 78b3b07498..21b3a86f97 100755 --- a/system/sensord/pigeond.py +++ b/system/sensord/pigeond.py @@ -7,7 +7,6 @@ import struct import requests import urllib.parse from datetime import datetime -from typing import List, Optional, Tuple from cereal import messaging from openpilot.common.params import Params @@ -41,7 +40,7 @@ def add_ubx_checksum(msg: bytes) -> bytes: B = (B + A) % 256 return msg + bytes([A, B]) -def get_assistnow_messages(token: bytes) -> List[bytes]: +def get_assistnow_messages(token: bytes) -> list[bytes]: # make request # TODO: implement adding the last known location r = requests.get("https://online-live2.services.u-blox.com/GetOnlineData.ashx", params=urllib.parse.urlencode({ @@ -238,7 +237,7 @@ def initialize_pigeon(pigeon: TTYPigeon) -> bool: return False return True -def deinitialize_and_exit(pigeon: Optional[TTYPigeon]): +def deinitialize_and_exit(pigeon: TTYPigeon | None): cloudlog.warning("Storing almanac in ublox flash") if pigeon is not None: @@ -259,7 +258,7 @@ def deinitialize_and_exit(pigeon: Optional[TTYPigeon]): set_power(False) sys.exit(0) -def create_pigeon() -> Tuple[TTYPigeon, messaging.PubMaster]: +def create_pigeon() -> tuple[TTYPigeon, messaging.PubMaster]: pigeon = None # register exit handler diff --git a/system/sensord/sensors_qcom2.cc b/system/sensord/sensors_qcom2.cc index 36d9b4a13e..9cbc24864d 100644 --- a/system/sensord/sensors_qcom2.cc +++ b/system/sensord/sensors_qcom2.cc @@ -134,7 +134,13 @@ int sensor_loop(I2CBus *i2c_bus_imu) { // increase interrupt quality by pinning interrupt and process to core 1 setpriority(PRIO_PROCESS, 0, -18); util::set_core_affinity({1}); - std::system("sudo su -c 'echo 1 > /proc/irq/336/smp_affinity_list'"); + + // TODO: get the IRQ number from gpiochip + std::string irq_path = "/proc/irq/336/smp_affinity_list"; + if (!util::file_exists(irq_path)) { + irq_path = "/proc/irq/335/smp_affinity_list"; + } + std::system(util::string_format("sudo su -c 'echo 1 > %s'", irq_path.c_str()).c_str()); // thread for reading events via interrupts threads.emplace_back(&interrupt_loop, std::ref(sensors_init)); diff --git a/system/sensord/tests/test_pigeond.py b/system/sensord/tests/test_pigeond.py index f2ab43bbb7..742e20bb90 100755 --- a/system/sensord/tests/test_pigeond.py +++ b/system/sensord/tests/test_pigeond.py @@ -40,7 +40,7 @@ class TestPigeond(unittest.TestCase): sm.update(1 * 1000) if sm.updated['ubloxRaw']: break - assert sm.rcv_frame['ubloxRaw'] > 0, "pigeond didn't start outputting messages in time" + assert sm.recv_frame['ubloxRaw'] > 0, "pigeond didn't start outputting messages in time" et = time.monotonic() - start_time assert et < 5, f"pigeond took {et:.1f}s to start" diff --git a/system/timed.py b/system/timed.py index 21fb47b680..39acb2ba12 100755 --- a/system/timed.py +++ b/system/timed.py @@ -65,10 +65,15 @@ def main() -> NoReturn: cloudlog.debug("Restoring timezone from param") set_timezone(tz) + pm = messaging.PubMaster(['clocks']) sm = messaging.SubMaster(['liveLocationKalman']) while True: sm.update(1000) + msg = messaging.new_message('clocks', valid=True) + msg.clocks.wallTimeNanos = time.time_ns() + pm.send('clocks', msg) + llk = sm['liveLocationKalman'] if not llk.gpsOK or (time.monotonic() - sm.logMonoTime['liveLocationKalman']/1e9) > 0.2: continue diff --git a/system/ubloxd/tests/ubloxd.py b/system/ubloxd/tests/ubloxd.py index 4ee99dc28a..c17387114f 100755 --- a/system/ubloxd/tests/ubloxd.py +++ b/system/ubloxd/tests/ubloxd.py @@ -82,7 +82,7 @@ def configure_ublox(dev): if __name__ == "__main__": class Device: def write(self, s): - d = '"{}"s'.format(''.join('\\x{:02X}'.format(b) for b in s)) + d = '"{}"s'.format(''.join(f'\\x{b:02X}' for b in s)) print(f" if (!send_with_ack({d})) continue;") dev = ublox.UBlox(Device(), baudrate=baudrate) diff --git a/system/version.py b/system/version.py index 980a4fcc7c..4319ef2140 100755 --- a/system/version.py +++ b/system/version.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 import os import subprocess -from typing import List, Optional, Callable, TypeVar +from typing import TypeVar +from collections.abc import Callable from functools import lru_cache from openpilot.common.basedir import BASEDIR @@ -18,11 +19,11 @@ def cache(user_function: Callable[..., _RT], /) -> Callable[..., _RT]: return lru_cache(maxsize=None)(user_function) -def run_cmd(cmd: List[str]) -> str: +def run_cmd(cmd: list[str]) -> str: return subprocess.check_output(cmd, encoding='utf8').strip() -def run_cmd_default(cmd: List[str], default: Optional[str] = None) -> Optional[str]: +def run_cmd_default(cmd: list[str], default: str = "") -> str: try: return run_cmd(cmd) except subprocess.CalledProcessError: @@ -31,17 +32,22 @@ def run_cmd_default(cmd: List[str], default: Optional[str] = None) -> Optional[s @cache def get_commit(branch: str = "HEAD") -> str: - return run_cmd_default(["git", "rev-parse", branch]) or "" + return run_cmd_default(["git", "rev-parse", branch]) + + +@cache +def get_commit_date(commit: str = "HEAD") -> str: + return run_cmd_default(["git", "show", "--no-patch", "--format='%ct %ci'", commit]) @cache def get_short_branch() -> str: - return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "HEAD"]) or "" + return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "HEAD"]) @cache def get_branch() -> str: - return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]) or "" + return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]) @cache @@ -51,7 +57,7 @@ def get_origin() -> str: tracking_remote = run_cmd(["git", "config", "branch." + local_branch + ".remote"]) return run_cmd(["git", "config", "remote." + tracking_remote + ".url"]) except subprocess.CalledProcessError: # Not on a branch, fallback - return run_cmd_default(["git", "config", "--get", "remote.origin.url"]) or "" + return run_cmd_default(["git", "config", "--get", "remote.origin.url"]) @cache @@ -132,3 +138,4 @@ if __name__ == "__main__": print(f"Branch: {get_branch()}") print(f"Short branch: {get_short_branch()}") print(f"Prebuilt: {is_prebuilt()}") + print(f"Commit date: {get_commit_date()}") diff --git a/system/webrtc/device/audio.py b/system/webrtc/device/audio.py index 3c78be6752..b1859518a1 100644 --- a/system/webrtc/device/audio.py +++ b/system/webrtc/device/audio.py @@ -1,6 +1,5 @@ import asyncio import io -from typing import Optional, List, Tuple import aiortc import av @@ -17,7 +16,7 @@ class AudioInputStreamTrack(aiortc.mediastreams.AudioStreamTrack): pyaudio.paFloat32: 'flt', } - def __init__(self, audio_format: int = pyaudio.paInt16, rate: int = 16000, channels: int = 1, packet_time: float = 0.020, device_index: Optional[int] = None): + def __init__(self, audio_format: int = pyaudio.paInt16, rate: int = 16000, channels: int = 1, packet_time: float = 0.020, device_index: int | None = None): super().__init__() self.p = pyaudio.PyAudio() @@ -49,7 +48,7 @@ class AudioInputStreamTrack(aiortc.mediastreams.AudioStreamTrack): class AudioOutputSpeaker: - def __init__(self, audio_format: int = pyaudio.paInt16, rate: int = 48000, channels: int = 2, packet_time: float = 0.2, device_index: Optional[int] = None): + def __init__(self, audio_format: int = pyaudio.paInt16, rate: int = 48000, channels: int = 2, packet_time: float = 0.2, device_index: int | None = None): chunk_size = int(packet_time * rate) self.p = pyaudio.PyAudio() @@ -62,7 +61,7 @@ class AudioOutputSpeaker: output=True, output_device_index=device_index, stream_callback=self.__pyaudio_callback) - self.tracks_and_tasks: List[Tuple[aiortc.MediaStreamTrack, Optional[asyncio.Task]]] = [] + self.tracks_and_tasks: list[tuple[aiortc.MediaStreamTrack, asyncio.Task | None]] = [] def __pyaudio_callback(self, in_data, frame_count, time_info, status): if self.buffer.getbuffer().nbytes < frame_count * self.channels * 2: diff --git a/system/webrtc/device/video.py b/system/webrtc/device/video.py index 314f812834..1bca909294 100644 --- a/system/webrtc/device/video.py +++ b/system/webrtc/device/video.py @@ -1,5 +1,4 @@ import asyncio -from typing import Optional import av from teleoprtc.tracks import TiciVideoStreamTrack @@ -40,5 +39,5 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): return packet - def codec_preference(self) -> Optional[str]: + def codec_preference(self) -> str | None: return "H264" diff --git a/system/webrtc/schema.py b/system/webrtc/schema.py index f659b34293..d80986ebf2 100644 --- a/system/webrtc/schema.py +++ b/system/webrtc/schema.py @@ -1,8 +1,8 @@ import capnp -from typing import Union, List, Dict, Any +from typing import Any -def generate_type(type_walker, schema_walker) -> Union[str, List[Any], Dict[str, Any]]: +def generate_type(type_walker, schema_walker) -> str | list[Any] | dict[str, Any]: data_type = next(type_walker) if data_type.which() == 'struct': return generate_struct(next(schema_walker)) @@ -15,11 +15,11 @@ def generate_type(type_walker, schema_walker) -> Union[str, List[Any], Dict[str, return str(data_type.which()) -def generate_struct(schema: capnp.lib.capnp._StructSchema) -> Dict[str, Any]: +def generate_struct(schema: capnp.lib.capnp._StructSchema) -> dict[str, Any]: return {field: generate_field(schema.fields[field]) for field in schema.fields if not field.endswith("DEPRECATED")} -def generate_field(field: capnp.lib.capnp._StructSchemaField) -> Union[str, List[Any], Dict[str, Any]]: +def generate_field(field: capnp.lib.capnp._StructSchemaField) -> str | list[Any] | dict[str, Any]: def schema_walker(field): yield field.schema diff --git a/system/webrtc/tests/test_webrtcd.py b/system/webrtc/tests/test_webrtcd.py index b48bf6bc19..c31b63d02b 100755 --- a/system/webrtc/tests/test_webrtcd.py +++ b/system/webrtc/tests/test_webrtcd.py @@ -18,13 +18,13 @@ class TestWebrtcdProc(unittest.IsolatedAsyncioTestCase): try: async with asyncio.timeout(timeout): await awaitable - except asyncio.TimeoutError: + except TimeoutError: self.fail("Timeout while waiting for awaitable to complete") async def test_webrtcd(self): mock_request = MagicMock() async def connect(offer): - body = {'sdp': offer.sdp, 'cameras': offer.video, 'bridge_services_in': [], 'bridge_services_out': []} + body = {'sdp': offer.sdp, 'cameras': offer.video, 'bridge_services_in': [], 'bridge_services_out': ['carState']} mock_request.json.side_effect = AsyncMock(return_value=body) response = await get_stream(mock_request) response_json = json.loads(response.text) diff --git a/system/webrtc/webrtcd.py b/system/webrtc/webrtcd.py index cc26d50daf..6c1370eae9 100755 --- a/system/webrtc/webrtcd.py +++ b/system/webrtc/webrtcd.py @@ -6,7 +6,7 @@ import json import uuid import logging from dataclasses import dataclass, field -from typing import Any, List, Optional, Union, TYPE_CHECKING +from typing import Any, TYPE_CHECKING # aiortc and its dependencies have lots of internal warnings :( import warnings @@ -24,7 +24,7 @@ from cereal import messaging, log class CerealOutgoingMessageProxy: def __init__(self, sm: messaging.SubMaster): self.sm = sm - self.channels: List['RTCDataChannel'] = [] + self.channels: list['RTCDataChannel'] = [] def add_channel(self, channel: 'RTCDataChannel'): self.channels.append(channel) @@ -103,7 +103,7 @@ class CerealProxyRunner: class StreamSession: - def __init__(self, sdp: str, cameras: List[str], incoming_services: List[str], outgoing_services: List[str], debug_mode: bool = False): + def __init__(self, sdp: str, cameras: list[str], incoming_services: list[str], outgoing_services: list[str], debug_mode: bool = False): from aiortc.mediastreams import VideoStreamTrack, AudioStreamTrack from aiortc.contrib.media import MediaBlackhole from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack @@ -132,8 +132,8 @@ class StreamSession: self.incoming_bridge = CerealIncomingMessageProxy(messaging.PubMaster(incoming_services)) self.outgoing_bridge_runner = CerealProxyRunner(self.outgoing_bridge) - self.audio_output: Optional[Union[AudioOutputSpeaker, MediaBlackhole]] = None - self.run_task: Optional[asyncio.Task] = None + self.audio_output: AudioOutputSpeaker | MediaBlackhole | None = None + self.run_task: asyncio.Task | None = None self.logger = logging.getLogger("webrtcd") self.logger.info("New stream session (%s), cameras %s, audio in %s out %s, incoming services %s, outgoing services %s", self.identifier, cameras, config.incoming_audio_track, config.expected_audio_track, incoming_services, outgoing_services) @@ -189,9 +189,9 @@ class StreamSession: @dataclass class StreamRequestBody: sdp: str - cameras: List[str] - bridge_services_in: List[str] = field(default_factory=list) - bridge_services_out: List[str] = field(default_factory=list) + cameras: list[str] + bridge_services_in: list[str] = field(default_factory=list) + bridge_services_out: list[str] = field(default_factory=list) async def get_stream(request: 'web.Request'): diff --git a/tools/bodyteleop/web.py b/tools/bodyteleop/web.py index b1fb9525db..fd8f691d19 100644 --- a/tools/bodyteleop/web.py +++ b/tools/bodyteleop/web.py @@ -56,7 +56,7 @@ def create_ssl_cert(cert_path: str, key_path: str): try: proc = subprocess.run(f'openssl req -x509 -newkey rsa:4096 -nodes -out {cert_path} -keyout {key_path} \ -days 365 -subj "/C=US/ST=California/O=commaai/OU=comma body"', - stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + capture_output=True, shell=True) proc.check_returncode() except subprocess.CalledProcessError as ex: raise ValueError(f"Error creating SSL certificate:\n[stdout]\n{proc.stdout.decode()}\n[stderr]\n{proc.stderr.decode()}") from ex @@ -77,7 +77,7 @@ def create_ssl_context(): ## ENDPOINTS async def index(request: 'web.Request'): - with open(os.path.join(TELEOPDIR, "static", "index.html"), "r") as f: + with open(os.path.join(TELEOPDIR, "static", "index.html")) as f: content = f.read() return web.Response(content_type="text/html", text=content) diff --git a/tools/camerastream/compressed_vipc.py b/tools/camerastream/compressed_vipc.py index f1a6f230fa..f4b8862a84 100755 --- a/tools/camerastream/compressed_vipc.py +++ b/tools/camerastream/compressed_vipc.py @@ -110,7 +110,7 @@ class CompressedVipc: os.environ["ZMQ"] = "1" messaging.context = messaging.Context() sm = messaging.SubMaster([ENCODE_SOCKETS[s] for s in vision_streams], addr=addr) - while min(sm.rcv_frame.values()) == 0: + while min(sm.recv_frame.values()) == 0: sm.update(100) os.environ.pop("ZMQ") messaging.context = messaging.Context() diff --git a/tools/car_porting/auto_fingerprint.py b/tools/car_porting/auto_fingerprint.py index 7dae9ce048..f122c2774e 100755 --- a/tools/car_porting/auto_fingerprint.py +++ b/tools/car_porting/auto_fingerprint.py @@ -2,7 +2,6 @@ import argparse from collections import defaultdict -from typing import Optional from openpilot.selfdrive.debug.format_fingerprints import format_brand_fw_versions from openpilot.selfdrive.car.fw_versions import match_fw_to_car @@ -30,7 +29,7 @@ if __name__ == "__main__": carVin = None carPlatform = None - platform: Optional[str] = None + platform: str | None = None CP = lr.first("carParams") diff --git a/tools/car_porting/test_car_model.py b/tools/car_porting/test_car_model.py index 66fe2ea65f..86980b054b 100755 --- a/tools/car_porting/test_car_model.py +++ b/tools/car_porting/test_car_model.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import argparse import sys -from typing import List import unittest from openpilot.selfdrive.car.tests.routes import CarTestRoute @@ -9,7 +8,7 @@ from openpilot.selfdrive.car.tests.test_models import TestCarModel from openpilot.tools.lib.route import SegmentName -def create_test_models_suite(routes: List[CarTestRoute], ci=False) -> unittest.TestSuite: +def create_test_models_suite(routes: list[CarTestRoute], ci=False) -> unittest.TestSuite: test_suite = unittest.TestSuite() for test_route in routes: # create new test case and discover tests diff --git a/tools/install_python_dependencies.sh b/tools/install_python_dependencies.sh index 6753afffb9..f7ba316480 100755 --- a/tools/install_python_dependencies.sh +++ b/tools/install_python_dependencies.sh @@ -75,12 +75,8 @@ pyenv rehash [ -n "$POETRY_VIRTUALENVS_CREATE" ] && RUN="" || RUN="poetry run" -if [ "$(uname)" != "Darwin" ]; then +if [ "$(uname)" != "Darwin" ] && [ -e "$ROOT/.git" ]; then echo "pre-commit hooks install..." - shopt -s nullglob - for f in .pre-commit-config.yaml */.pre-commit-config.yaml; do - if [ -e "$ROOT/$(dirname $f)/.git" ]; then - $RUN pre-commit install -c "$f" - fi - done + $RUN pre-commit install + $RUN git submodule foreach pre-commit install fi diff --git a/tools/latencylogger/latency_logger.py b/tools/latencylogger/latency_logger.py index 19c0a86bf4..8c6af56b6e 100755 --- a/tools/latencylogger/latency_logger.py +++ b/tools/latencylogger/latency_logger.py @@ -90,7 +90,7 @@ def read_logs(lr): return (data, frame_mismatches) # This is not needed in 3.10 as a "key" parameter is added to bisect -class KeyifyList(object): +class KeyifyList: def __init__(self, inner, key): self.inner = inner self.key = key diff --git a/tools/lib/README.md b/tools/lib/README.md index a0c4a0863b..af1ad0de22 100644 --- a/tools/lib/README.md +++ b/tools/lib/README.md @@ -34,10 +34,16 @@ for msg in lr: ### Segment Ranges -We also support a new format called a "segment range", where you can specify which segments from a route to load. +We also support a new format called a "segment range": -```python +``` +344c5c15b34f2d8a / 2024-01-03--09-37-12 / 2:6 / q +[ dongle id ] [ timestamp ] [ selector ] [ query type] +``` +you can specify which segments from a route to load + +```python lr = LogReader("a2a0ccea32023010|2023-07-27--13-01-19/4") # 4th segment lr = LogReader("a2a0ccea32023010|2023-07-27--13-01-19/4:6") # 4th and 5th segment lr = LogReader("a2a0ccea32023010|2023-07-27--13-01-19/-1") # last segment diff --git a/tools/lib/auth.py b/tools/lib/auth.py index 997d1f860d..5988397d0a 100755 --- a/tools/lib/auth.py +++ b/tools/lib/auth.py @@ -26,7 +26,7 @@ import sys import pprint import webbrowser from http.server import BaseHTTPRequestHandler, HTTPServer -from typing import Any, Dict +from typing import Any from urllib.parse import parse_qs, urlencode from openpilot.tools.lib.api import APIError, CommaApi, UnauthorizedError @@ -36,7 +36,7 @@ PORT = 3000 class ClientRedirectServer(HTTPServer): - query_params: Dict[str, Any] = {} + query_params: dict[str, Any] = {} class ClientRedirectHandler(BaseHTTPRequestHandler): diff --git a/tools/lib/azure_container.py b/tools/lib/azure_container.py index 7d9550266d..52b2f37dbf 100644 --- a/tools/lib/azure_container.py +++ b/tools/lib/azure_container.py @@ -2,7 +2,7 @@ import os from datetime import datetime, timedelta from functools import lru_cache from pathlib import Path -from typing import IO, Union +from typing import IO TOKEN_PATH = Path("/data/azure_token") @@ -57,7 +57,7 @@ class AzureContainer: ext = "hevc" if log_type.endswith('camera') else "bz2" return self.BASE_URL + f"{route_name.replace('|', '/')}/{segment_num}/{log_type}.{ext}" - def upload_bytes(self, data: Union[bytes, IO], blob_name: str) -> str: + def upload_bytes(self, data: bytes | IO, blob_name: str) -> str: from azure.storage.blob import BlobClient blob = BlobClient( account_url=self.ACCOUNT_URL, @@ -69,6 +69,6 @@ class AzureContainer: blob.upload_blob(data) return self.BASE_URL + blob_name - def upload_file(self, path: Union[str, os.PathLike], blob_name: str) -> str: + def upload_file(self, path: str | os.PathLike, blob_name: str) -> str: with open(path, "rb") as f: return self.upload_bytes(f, blob_name) diff --git a/tools/lib/bootlog.py b/tools/lib/bootlog.py index 827ef1eefc..208ddc19c2 100644 --- a/tools/lib/bootlog.py +++ b/tools/lib/bootlog.py @@ -1,6 +1,5 @@ import functools import re -from typing import List, Optional from openpilot.tools.lib.auth_config import get_token from openpilot.tools.lib.api import CommaApi @@ -44,7 +43,7 @@ class Bootlog: return False return self.id < b.id -def get_bootlog_from_id(bootlog_id: str) -> Optional[Bootlog]: +def get_bootlog_from_id(bootlog_id: str) -> Bootlog | None: # TODO: implement an API endpoint for this bl = Bootlog(bootlog_id) for b in get_bootlogs(bl.dongle_id): @@ -52,7 +51,7 @@ def get_bootlog_from_id(bootlog_id: str) -> Optional[Bootlog]: return b return None -def get_bootlogs(dongle_id: str) -> List[Bootlog]: +def get_bootlogs(dongle_id: str) -> list[Bootlog]: api = CommaApi(get_token()) r = api.get(f'v1/devices/{dongle_id}/bootlogs') return [Bootlog(b) for b in r] diff --git a/tools/lib/filereader.py b/tools/lib/filereader.py index 1db3207e4b..e9b8b4b2ce 100644 --- a/tools/lib/filereader.py +++ b/tools/lib/filereader.py @@ -1,20 +1,36 @@ import os +import socket +from urllib.parse import urlparse from openpilot.tools.lib.url_file import URLFile DATA_ENDPOINT = os.getenv("DATA_ENDPOINT", "http://data-raw.comma.internal/") + +def internal_source_available(): + try: + hostname = urlparse(DATA_ENDPOINT).hostname + if hostname: + socket.gethostbyname(hostname) + return True + except socket.gaierror: + pass + return False + + def resolve_name(fn): if fn.startswith("cd:/"): return fn.replace("cd:/", DATA_ENDPOINT) return fn + def file_exists(fn): fn = resolve_name(fn) if fn.startswith(("http://", "https://")): return URLFile(fn).get_length_online() != -1 return os.path.exists(fn) + def FileReader(fn, debug=False): fn = resolve_name(fn) if fn.startswith(("http://", "https://")): diff --git a/tools/lib/helpers.py b/tools/lib/helpers.py index 423f207b4d..653eb3d7e0 100644 --- a/tools/lib/helpers.py +++ b/tools/lib/helpers.py @@ -3,23 +3,24 @@ import datetime TIME_FMT = "%Y-%m-%d--%H-%M-%S" + # regex patterns class RE: - DONGLE_ID = r'(?P[a-z0-9]{16})' + DONGLE_ID = r'(?P[a-f0-9]{16})' TIMESTAMP = r'(?P[0-9]{4}-[0-9]{2}-[0-9]{2}--[0-9]{2}-[0-9]{2}-[0-9]{2})' - LOG_ID_V2 = r'(?P[a-z0-9]{8})--(?P[a-z0-9]{10})' - LOG_ID = r'(?P(?:{}|{}))'.format(TIMESTAMP, LOG_ID_V2) - ROUTE_NAME = r'(?P{}[|_/]{})'.format(DONGLE_ID, LOG_ID) - SEGMENT_NAME = r'{}(?:--|/)(?P[0-9]+)'.format(ROUTE_NAME) + LOG_ID_V2 = r'(?P[a-f0-9]{8})--(?P[a-z0-9]{10})' + LOG_ID = fr'(?P(?:{TIMESTAMP}|{LOG_ID_V2}))' + ROUTE_NAME = fr'(?P{DONGLE_ID}[|_/]{LOG_ID})' + SEGMENT_NAME = fr'{ROUTE_NAME}(?:--|/)(?P[0-9]+)' INDEX = r'-?[0-9]+' - SLICE = r'(?P{})?:?(?P{})?:?(?P{})?'.format(INDEX, INDEX, INDEX) - SEGMENT_RANGE = r'{}(?:--|/)?(?P({}))?/?(?P([qras]))?'.format(ROUTE_NAME, SLICE) + SLICE = fr'(?P{INDEX})?:?(?P{INDEX})?:?(?P{INDEX})?' + SEGMENT_RANGE = fr'{ROUTE_NAME}(?:(--|/)(?P({SLICE})))?(?:/(?P([qras])))?' BOOTLOG_NAME = ROUTE_NAME - EXPLORER_FILE = r'^(?P{})--(?P[a-z]+\.[a-z0-9]+)$'.format(SEGMENT_NAME) - OP_SEGMENT_DIR = r'^(?P{})$'.format(SEGMENT_NAME) + EXPLORER_FILE = fr'^(?P{SEGMENT_NAME})--(?P[a-z]+\.[a-z0-9]+)$' + OP_SEGMENT_DIR = fr'^(?P{SEGMENT_NAME})$' def timestamp_to_datetime(t: str) -> datetime.datetime: diff --git a/tools/lib/live_logreader.py b/tools/lib/live_logreader.py new file mode 100644 index 0000000000..6a7ecee6fd --- /dev/null +++ b/tools/lib/live_logreader.py @@ -0,0 +1,30 @@ +import os +from cereal import log as capnp_log, messaging +from cereal.services import SERVICE_LIST + +from openpilot.tools.lib.logreader import LogIterable, RawLogIterable + + +ALL_SERVICES = list(SERVICE_LIST.keys()) + +def raw_live_logreader(services: list[str] = ALL_SERVICES, addr: str = '127.0.0.1') -> RawLogIterable: + if addr != "127.0.0.1": + os.environ["ZMQ"] = "1" + messaging.context = messaging.Context() + + poller = messaging.Poller() + + for m in services: + messaging.sub_sock(m, poller, addr=addr) + + while True: + polld = poller.poll(100) + for sock in polld: + msg = sock.receive() + yield msg + + +def live_logreader(services: list[str] = ALL_SERVICES, addr: str = '127.0.0.1') -> LogIterable: + for m in raw_live_logreader(services, addr): + with capnp_log.Event.from_bytes(m) as evt: + yield evt diff --git a/tools/lib/logreader.py b/tools/lib/logreader.py index decffccd66..6247bbc9db 100755 --- a/tools/lib/logreader.py +++ b/tools/lib/logreader.py @@ -4,28 +4,26 @@ from functools import partial import multiprocessing import capnp import enum -import numpy as np import os import pathlib -import re import sys import tqdm import urllib.parse import warnings -from typing import Dict, Iterable, Iterator, List, Type +from collections.abc import Callable, Iterable, Iterator from urllib.parse import parse_qs, urlparse from cereal import log as capnp_log from openpilot.common.swaglog import cloudlog from openpilot.tools.lib.comma_car_segments import get_url as get_comma_segments_url from openpilot.tools.lib.openpilotci import get_url -from openpilot.tools.lib.filereader import FileReader, file_exists -from openpilot.tools.lib.helpers import RE +from openpilot.tools.lib.filereader import FileReader, file_exists, internal_source_available from openpilot.tools.lib.route import Route, SegmentRange -LogMessage = Type[capnp._DynamicStructReader] +LogMessage = type[capnp._DynamicStructReader] LogIterable = Iterable[LogMessage] +RawLogIterable = Iterable[bytes] class _LogFileReader: @@ -71,28 +69,25 @@ class _LogFileReader: class ReadMode(enum.StrEnum): - RLOG = "r" # only read rlogs - QLOG = "q" # only read qlogs - SANITIZED = "s" # read from the commaCarSegments database - AUTO = "a" # default to rlogs, fallback to qlogs - AUTO_INTERACIVE = "i" # default to rlogs, fallback to qlogs with a prompt from the user - -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 default_valid_file(fn): + RLOG = "r" # only read rlogs + QLOG = "q" # only read qlogs + SANITIZED = "s" # read from the commaCarSegments database + AUTO = "a" # default to rlogs, fallback to qlogs + AUTO_INTERACTIVE = "i" # default to rlogs, fallback to qlogs with a prompt from the user + + +LogPath = str | None +LogPaths = list[LogPath] +ValidFileCallable = Callable[[LogPath], bool] +Source = Callable[[SegmentRange, ReadMode], LogPaths] + +InternalUnavailableException = Exception("Internal source not available") + +def default_valid_file(fn: LogPath) -> bool: return fn is not None and file_exists(fn) -def auto_strategy(rlog_paths, qlog_paths, interactive, valid_file): + +def auto_strategy(rlog_paths: LogPaths, qlog_paths: LogPaths, interactive: bool, valid_file: ValidFileCallable) -> LogPaths: # auto select logs based on availability if any(rlog is None or not valid_file(rlog) for rlog in rlog_paths): if interactive: @@ -101,40 +96,28 @@ def auto_strategy(rlog_paths, qlog_paths, interactive, valid_file): else: cloudlog.warning("Some rlogs were not found, falling back to qlogs for those segments...") - return [rlog if (valid_file(rlog)) else (qlog if (valid_file(qlog)) else None) - for (rlog, qlog) in zip(rlog_paths, qlog_paths, strict=True)] + return [rlog if valid_file(rlog) else (qlog if valid_file(qlog) else None) + for (rlog, qlog) in zip(rlog_paths, qlog_paths, strict=True)] return rlog_paths -def apply_strategy(mode: ReadMode, rlog_paths, qlog_paths, valid_file=default_valid_file): + +def apply_strategy(mode: ReadMode, rlog_paths: LogPaths, qlog_paths: LogPaths, valid_file: ValidFileCallable = default_valid_file) -> LogPaths: if mode == ReadMode.RLOG: return rlog_paths elif mode == ReadMode.QLOG: return qlog_paths elif mode == ReadMode.AUTO: return auto_strategy(rlog_paths, qlog_paths, False, valid_file) - elif mode == ReadMode.AUTO_INTERACIVE: + elif mode == ReadMode.AUTO_INTERACTIVE: return auto_strategy(rlog_paths, qlog_paths, True, valid_file) + raise Exception(f"invalid mode: {mode}") -def parse_slice(sr: SegmentRange): - s = create_slice_from_string(sr._slice) - if isinstance(s, slice): - if s.stop is None or s.stop < 0 or (s.start is not None and s.start < 0): # we need the number of segments in order to parse this slice - segs = np.arange(sr.get_max_seg_number()+1) - else: - segs = np.arange(s.stop + 1) - return segs[s] - else: - if s < 0: - s = sr.get_max_seg_number() + s + 1 - return [s] - -def comma_api_source(sr: SegmentRange, mode: ReadMode): - segs = parse_slice(sr) +def comma_api_source(sr: SegmentRange, mode: ReadMode) -> LogPaths: route = Route(sr.route_name) - rlog_paths = [route.log_paths()[seg] for seg in segs] - qlog_paths = [route.qlog_paths()[seg] for seg in segs] + rlog_paths = [route.log_paths()[seg] for seg in sr.seg_idxs] + qlog_paths = [route.qlog_paths()[seg] for seg in sr.seg_idxs] # comma api will have already checked if the file exists def valid_file(fn): @@ -142,79 +125,84 @@ def comma_api_source(sr: SegmentRange, mode: ReadMode): return apply_strategy(mode, rlog_paths, qlog_paths, valid_file=valid_file) -def internal_source(sr: SegmentRange, mode: ReadMode): - segs = parse_slice(sr) + +def internal_source(sr: SegmentRange, mode: ReadMode) -> LogPaths: + if not internal_source_available(): + raise InternalUnavailableException def get_internal_url(sr: SegmentRange, seg, file): return f"cd:/{sr.dongle_id}/{sr.timestamp}/{seg}/{file}.bz2" - rlog_paths = [get_internal_url(sr, seg, "rlog") for seg in segs] - qlog_paths = [get_internal_url(sr, seg, "qlog") for seg in segs] + rlog_paths = [get_internal_url(sr, seg, "rlog") for seg in sr.seg_idxs] + qlog_paths = [get_internal_url(sr, seg, "qlog") for seg in sr.seg_idxs] return apply_strategy(mode, rlog_paths, qlog_paths) -def openpilotci_source(sr: SegmentRange, mode: ReadMode): - segs = parse_slice(sr) - rlog_paths = [get_url(sr.route_name, seg, "rlog") for seg in segs] - qlog_paths = [get_url(sr.route_name, seg, "qlog") for seg in segs] +def openpilotci_source(sr: SegmentRange, mode: ReadMode) -> LogPaths: + rlog_paths = [get_url(sr.route_name, seg, "rlog") for seg in sr.seg_idxs] + qlog_paths = [get_url(sr.route_name, seg, "qlog") for seg in sr.seg_idxs] return apply_strategy(mode, rlog_paths, qlog_paths) -def comma_car_segments_source(sr: SegmentRange, mode=ReadMode.RLOG): - segs = parse_slice(sr) - return [get_comma_segments_url(sr.route_name, seg) for seg in segs] +def comma_car_segments_source(sr: SegmentRange, mode=ReadMode.RLOG) -> LogPaths: + return [get_comma_segments_url(sr.route_name, seg) for seg in sr.seg_idxs] -def direct_source(file_or_url): + +def direct_source(file_or_url: str) -> LogPaths: return [file_or_url] + def get_invalid_files(files): for f in files: if f is None or not file_exists(f): yield f -def check_source(source, *args): - try: - files = source(*args) - assert next(get_invalid_files(files), None) is None - return None, files - except Exception as e: - return e, None -def auto_source(sr: SegmentRange, mode=ReadMode.RLOG): +def check_source(source: Source, *args) -> LogPaths: + files = source(*args) + assert next(get_invalid_files(files), False) is False + return files + + +def auto_source(sr: SegmentRange, mode=ReadMode.RLOG) -> LogPaths: if mode == ReadMode.SANITIZED: return comma_car_segments_source(sr, mode) + SOURCES: list[Source] = [internal_source, openpilotci_source, comma_api_source, comma_car_segments_source,] exceptions = [] # Automatically determine viable source - for source in [internal_source, openpilotci_source, comma_api_source, comma_car_segments_source]: - exception, ret = check_source(source, sr, mode) - if exception is None: - return ret - else: - exceptions.append(exception) + for source in SOURCES: + try: + return check_source(source, sr, mode) + except Exception as e: + exceptions.append(e) raise Exception(f"auto_source could not find any valid source, exceptions for sources: {exceptions}") -def parse_useradmin(identifier): + +def parse_useradmin(identifier: str): if "useradmin.comma.ai" in identifier: query = parse_qs(urlparse(identifier).query) return query["onebox"][0] return None -def parse_cabana(identifier): + +def parse_cabana(identifier: str): if "cabana.comma.ai" in identifier: query = parse_qs(urlparse(identifier).query) return query["route"][0] return None -def parse_direct(identifier): + +def parse_direct(identifier: str): if identifier.startswith(("http://", "https://", "cd:/")) or pathlib.Path(identifier).exists(): return identifier return None -def parse_indirect(identifier): + +def parse_indirect(identifier: str): parsed = parse_useradmin(identifier) or parse_cabana(identifier) if parsed is not None: @@ -224,7 +212,7 @@ def parse_indirect(identifier): class LogReader: - def _parse_identifiers(self, identifier: str | List[str]): + def _parse_identifiers(self, identifier: str | list[str]): if isinstance(identifier, list): return [i for j in identifier for i in self._parse_identifiers(j)] @@ -239,9 +227,15 @@ class LogReader: 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) + identifiers = source(sr, mode) + + invalid_count = len(list(get_invalid_files(identifiers))) + assert invalid_count == 0, f"{invalid_count}/{len(identifiers)} invalid log(s) found, please ensure all logs \ +are uploaded or auto fallback to qlogs with '/a' selector at the end of the route name." + return identifiers - def __init__(self, identifier: str | List[str], default_mode=ReadMode.RLOG, default_source=auto_source, sort_by_time=False, only_union_types=False): + def __init__(self, identifier: str | list[str], default_mode: ReadMode = ReadMode.RLOG, + default_source=auto_source, sort_by_time=False, only_union_types=False): self.default_mode = default_mode self.default_source = default_source self.identifier = identifier @@ -249,7 +243,7 @@ class LogReader: self.sort_by_time = sort_by_time self.only_union_types = only_union_types - self.__lrs: Dict[int, _LogFileReader] = {} + self.__lrs: dict[int, _LogFileReader] = {} self.reset() def _get_lr(self, i): @@ -274,9 +268,6 @@ class LogReader: def reset(self): self.logreader_identifiers = self._parse_identifiers(self.identifier) - invalid_count = len(list(get_invalid_files(self.logreader_identifiers))) - assert invalid_count == 0, f"{invalid_count}/{len(self.logreader_identifiers)} invalid log(s) found, please ensure all logs \ -are uploaded or auto fallback to qlogs with '/a' selector at the end of the route name." @staticmethod def from_bytes(dat): @@ -291,6 +282,7 @@ are uploaded or auto fallback to qlogs with '/a' selector at the end of the rout if __name__ == "__main__": import codecs + # capnproto <= 0.8.0 throws errors converting byte data to string # below line catches those errors and replaces the bytes with \x__ codecs.register_error("strict", codecs.backslashreplace_errors) diff --git a/tools/lib/route.py b/tools/lib/route.py index f7c3c432c8..bd0ccc1fc3 100644 --- a/tools/lib/route.py +++ b/tools/lib/route.py @@ -4,7 +4,7 @@ from functools import cache from urllib.parse import urlparse from collections import defaultdict from itertools import chain -from typing import Optional +from typing import cast from openpilot.tools.lib.auth_config import get_token from openpilot.tools.lib.api import CommaApi @@ -17,6 +17,7 @@ CAMERA_FILENAMES = ['fcamera.hevc', 'video.hevc'] DCAMERA_FILENAMES = ['dcamera.hevc'] ECAMERA_FILENAMES = ['ecamera.hevc'] + class Route: def __init__(self, name, data_dir=None): self._name = RouteName(name) @@ -37,27 +38,27 @@ class Route: def log_paths(self): log_path_by_seg_num = {s.name.segment_num: s.log_path for s in self._segments} - return [log_path_by_seg_num.get(i, None) for i in range(self.max_seg_number+1)] + return [log_path_by_seg_num.get(i, None) for i in range(self.max_seg_number + 1)] def qlog_paths(self): qlog_path_by_seg_num = {s.name.segment_num: s.qlog_path for s in self._segments} - return [qlog_path_by_seg_num.get(i, None) for i in range(self.max_seg_number+1)] + return [qlog_path_by_seg_num.get(i, None) for i in range(self.max_seg_number + 1)] def camera_paths(self): camera_path_by_seg_num = {s.name.segment_num: s.camera_path for s in self._segments} - return [camera_path_by_seg_num.get(i, None) for i in range(self.max_seg_number+1)] + return [camera_path_by_seg_num.get(i, None) for i in range(self.max_seg_number + 1)] def dcamera_paths(self): dcamera_path_by_seg_num = {s.name.segment_num: s.dcamera_path for s in self._segments} - return [dcamera_path_by_seg_num.get(i, None) for i in range(self.max_seg_number+1)] + return [dcamera_path_by_seg_num.get(i, None) for i in range(self.max_seg_number + 1)] def ecamera_paths(self): ecamera_path_by_seg_num = {s.name.segment_num: s.ecamera_path for s in self._segments} - return [ecamera_path_by_seg_num.get(i, None) for i in range(self.max_seg_number+1)] + return [ecamera_path_by_seg_num.get(i, None) for i in range(self.max_seg_number + 1)] def qcamera_paths(self): qcamera_path_by_seg_num = {s.name.segment_num: s.qcamera_path for s in self._segments} - return [qcamera_path_by_seg_num.get(i, None) for i in range(self.max_seg_number+1)] + return [qcamera_path_by_seg_num.get(i, None) for i in range(self.max_seg_number + 1)] # TODO: refactor this, it's super repetitive def _get_segments_remote(self): @@ -159,6 +160,7 @@ class Route: raise ValueError(f'Could not find segments for route {self.name.canonical_name} in data directory {data_dir}') return sorted(segments, key=lambda seg: seg.name.segment_num) + class Segment: def __init__(self, name, log_path, qlog_path, camera_path, dcamera_path, ecamera_path, qcamera_path): self._name = SegmentName(name) @@ -173,6 +175,7 @@ class Segment: def name(self): return self._name + class RouteName: def __init__(self, name_str: str): self._name_str = name_str @@ -194,6 +197,7 @@ class RouteName: def __str__(self) -> str: return self._canonical_name + class SegmentName: # TODO: add constructor that takes dongle_id, time_str, segment_num and then create instances # of this class instead of manually constructing a segment name (use canonical_name prop instead) @@ -206,7 +210,7 @@ class SegmentName: seg_num_delim = "--" if self._name_str.count("--") == 2 else "/" name_parts = self._name_str.rsplit(seg_num_delim, 1) if allow_route_name and len(name_parts) == 1: - name_parts.append("-1") # no segment number + name_parts.append("-1") # no segment number self._route_name = RouteName(name_parts[0]) self._num = int(name_parts[1]) self._canonical_name = f"{self._route_name._dongle_id}|{self._route_name._time_str}--{self._num}" @@ -227,47 +231,67 @@ class SegmentName: def route_name(self) -> RouteName: return self._route_name @property - def data_dir(self) -> Optional[str]: return self._data_dir + def data_dir(self) -> str | None: return self._data_dir def __str__(self) -> str: return self._canonical_name @cache -def get_max_seg_number_cached(sr: 'SegmentRange'): +def get_max_seg_number_cached(sr: 'SegmentRange') -> int: try: api = CommaApi(get_token()) - return api.get("/v1/route/" + sr.route_name.replace("/", "|"))["segment_numbers"][-1] + return cast(int, api.get("/v1/route/" + sr.route_name.replace("/", "|"))["segment_numbers"][-1]) except Exception as e: raise Exception("unable to get max_segment_number. ensure you have access to this route or the route is public.") from e 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}" - - def get_max_seg_number(self): - return get_max_seg_number_cached(self) + m = re.fullmatch(RE.SEGMENT_RANGE, segment_range) + assert m is not None, f"Segment range is not valid {segment_range}" + self.m = m @property - def route_name(self): + def route_name(self) -> str: return self.m.group("route_name") @property - def dongle_id(self): + def dongle_id(self) -> str: return self.m.group("dongle_id") @property - def timestamp(self): + def timestamp(self) -> str: return self.m.group("timestamp") @property - def _slice(self): - return self.m.group("slice") + def slice(self) -> str: + return self.m.group("slice") or "" @property - def selector(self): + def selector(self) -> str | None: return self.m.group("selector") - def __str__(self): - return f"{self.dongle_id}/{self.timestamp}" + (f"/{self._slice}" if self._slice else "") + (f"/{self.selector}" if self.selector else "") + @property + def seg_idxs(self) -> list[int]: + m = re.fullmatch(RE.SLICE, self.slice) + assert m is not None, f"Invalid slice: {self.slice}" + start, end, step = (None if s is None else int(s) for s in m.groups()) + + # one segment specified + if start is not None and end is None and ':' not in self.slice: + if start < 0: + start += get_max_seg_number_cached(self) + 1 + return [start] + + s = slice(start, end, step) + # no specified end or using relative indexing, need number of segments + if end is None or end < 0 or (start is not None and start < 0): + return list(range(get_max_seg_number_cached(self) + 1))[s] + else: + return list(range(end + 1))[s] + + def __str__(self) -> str: + return f"{self.dongle_id}/{self.timestamp}" + (f"/{self.slice}" if self.slice else "") + (f"/{self.selector}" if self.selector else "") + + def __repr__(self) -> str: + return self.__str__() diff --git a/tools/lib/tests/test_caching.py b/tools/lib/tests/test_caching.py index 294c5a2233..bc92b01357 100755 --- a/tools/lib/tests/test_caching.py +++ b/tools/lib/tests/test_caching.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 -from functools import wraps +from functools import partial import http.server import os -import threading -import time +import shutil +import socket import unittest from parameterized import parameterized - +from openpilot.selfdrive.athena.tests.helpers import with_http_server +from openpilot.system.hardware.hw import Paths from openpilot.tools.lib.url_file import URLFile @@ -16,7 +17,7 @@ class CachingTestRequestHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): if self.FILE_EXISTS: - self.send_response(200, b'1234') + self.send_response(206 if "Range" in self.headers else 200, b'1234') else: self.send_response(404) self.end_headers() @@ -30,31 +31,39 @@ class CachingTestRequestHandler(http.server.BaseHTTPRequestHandler): 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 +with_caching_server = partial(with_http_server, handler=CachingTestRequestHandler) class TestFileDownload(unittest.TestCase): + @with_caching_server + def test_pipeline_defaults(self, host): + # TODO: parameterize the defaults so we don't rely on hard-coded values in xx + + self.assertEqual(URLFile.pool_manager().pools._maxsize, 10) # PoolManager num_pools param + pool_manager_defaults = { + "maxsize": 100, + "socket_options": [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),], + } + for k, v in pool_manager_defaults.items(): + self.assertEqual(URLFile.pool_manager().connection_pool_kw.get(k), v) + + retry_defaults = { + "total": 5, + "backoff_factor": 0.5, + "status_forcelist": [409, 429, 503, 504], + } + for k, v in retry_defaults.items(): + self.assertEqual(getattr(URLFile.pool_manager().connection_pool_kw["retries"], k), v) + + # ensure caching off by default and cache dir doesn't get created + os.environ.pop("FILEREADER_CACHE", None) + if os.path.exists(Paths.download_cache_root()): + shutil.rmtree(Paths.download_cache_root()) + URLFile(f"{host}/test.txt").get_length() + URLFile(f"{host}/test.txt").read() + self.assertEqual(os.path.exists(Paths.download_cache_root()), False) + def compare_loads(self, url, start=0, length=None): """Compares range between cached and non cached version""" file_cached = URLFile(url, cache=True) @@ -110,10 +119,10 @@ class TestFileDownload(unittest.TestCase): @parameterized.expand([(True, ), (False, )]) @with_caching_server - def test_recover_from_missing_file(self, cache_enabled, port): + def test_recover_from_missing_file(self, cache_enabled, host): os.environ["FILEREADER_CACHE"] = "1" if cache_enabled else "0" - file_url = f"http://localhost:{port}/test.png" + file_url = f"{host}/test.png" CachingTestRequestHandler.FILE_EXISTS = False length = URLFile(file_url).get_length() diff --git a/tools/lib/tests/test_comma_car_segments.py b/tools/lib/tests/test_comma_car_segments.py index 484a4aae08..50d4200b4f 100644 --- a/tools/lib/tests/test_comma_car_segments.py +++ b/tools/lib/tests/test_comma_car_segments.py @@ -1,5 +1,3 @@ - - import unittest import requests @@ -25,7 +23,7 @@ class TestCommaCarSegments(unittest.TestCase): sr = SegmentRange(segment) - url = get_url(sr.route_name, sr._slice) + url = get_url(sr.route_name, sr.slice) resp = requests.get(url) self.assertEqual(resp.status_code, 200) diff --git a/tools/lib/tests/test_logreader.py b/tools/lib/tests/test_logreader.py old mode 100644 new mode 100755 index 7a8ea20b76..2141915b87 --- a/tools/lib/tests/test_logreader.py +++ b/tools/lib/tests/test_logreader.py @@ -1,6 +1,8 @@ +#!/usr/bin/env python3 +import contextlib +import io import shutil import tempfile -import numpy as np import os import unittest import pytest @@ -9,11 +11,12 @@ import requests from parameterized import parameterized from unittest import mock -from openpilot.tools.lib.logreader import LogIterable, LogReader, parse_indirect, parse_slice, ReadMode +from openpilot.tools.lib.logreader import LogIterable, LogReader, comma_api_source, parse_indirect, ReadMode, InternalUnavailableException from openpilot.tools.lib.route import SegmentRange +from openpilot.tools.lib.url_file import URLFileException -NUM_SEGS = 17 # number of segments in the test route -ALL_SEGS = list(np.arange(NUM_SEGS)) +NUM_SEGS = 17 # number of segments in the test route +ALL_SEGS = list(range(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" @@ -22,6 +25,24 @@ def noop(segment: LogIterable): return segment +@contextlib.contextmanager +def setup_source_scenario(is_internal=False): + with ( + mock.patch("openpilot.tools.lib.logreader.internal_source") as internal_source_mock, + mock.patch("openpilot.tools.lib.logreader.openpilotci_source") as openpilotci_source_mock, + mock.patch("openpilot.tools.lib.logreader.comma_api_source") as comma_api_source_mock, + ): + if is_internal: + internal_source_mock.return_value = [QLOG_FILE] + else: + internal_source_mock.side_effect = InternalUnavailableException + + openpilotci_source_mock.return_value = [None] + comma_api_source_mock.return_value = [QLOG_FILE] + + yield + + class TestLogReader(unittest.TestCase): @parameterized.expand([ (f"{TEST_ROUTE}", ALL_SEGS), @@ -51,8 +72,7 @@ class TestLogReader(unittest.TestCase): def test_indirect_parsing(self, identifier, expected): parsed, _, _ = parse_indirect(identifier) sr = SegmentRange(parsed) - segs = parse_slice(sr) - self.assertListEqual(list(segs), expected) + self.assertListEqual(list(sr.seg_idxs), expected, identifier) @parameterized.expand([ (f"{TEST_ROUTE}", f"{TEST_ROUTE}"), @@ -66,7 +86,10 @@ class TestLogReader(unittest.TestCase): sr = SegmentRange(identifier) self.assertEqual(str(sr), expected) - def test_direct_parsing(self): + @parameterized.expand([(True,), (False,)]) + @mock.patch("openpilot.tools.lib.logreader.file_exists") + def test_direct_parsing(self, cache_enabled, file_exists_mock): + os.environ["FILEREADER_CACHE"] = "1" if cache_enabled else "0" qlog = tempfile.NamedTemporaryFile(mode='wb', delete=False) with requests.get(QLOG_FILE, stream=True) as r: @@ -77,6 +100,12 @@ class TestLogReader(unittest.TestCase): l = len(list(LogReader(f))) self.assertGreater(l, 100) + with self.assertRaises(URLFileException) if not cache_enabled else self.assertRaises(AssertionError): + l = len(list(LogReader(QLOG_FILE.replace("/3/", "/200/")))) + + # file_exists should not be called for direct files + self.assertEqual(file_exists_mock.call_count, 0) + @parameterized.expand([ (f"{TEST_ROUTE}///",), (f"{TEST_ROUTE}---",), @@ -85,11 +114,26 @@ class TestLogReader(unittest.TestCase): (f"{TEST_ROUTE}/j",), (f"{TEST_ROUTE}/0:1:2:3",), (f"{TEST_ROUTE}/:::3",), + (f"{TEST_ROUTE}3",), + (f"{TEST_ROUTE}-3",), + (f"{TEST_ROUTE}--3a",), ]) def test_bad_ranges(self, segment_range): with self.assertRaises(AssertionError): - sr = SegmentRange(segment_range) - parse_slice(sr) + _ = SegmentRange(segment_range).seg_idxs + + @parameterized.expand([ + (f"{TEST_ROUTE}/0", False), + (f"{TEST_ROUTE}/:2", False), + (f"{TEST_ROUTE}/0:", True), + (f"{TEST_ROUTE}/-1", True), + (f"{TEST_ROUTE}", True), + ]) + def test_slicing_api_call(self, segment_range, api_call): + with mock.patch("openpilot.tools.lib.route.get_max_seg_number_cached") as max_seg_mock: + max_seg_mock.return_value = NUM_SEGS + _ = SegmentRange(segment_range).seg_idxs + self.assertEqual(api_call, max_seg_mock.called) @pytest.mark.slow def test_modes(self): @@ -110,7 +154,7 @@ class TestLogReader(unittest.TestCase): qlog_len = len(list(LogReader(f"{TEST_ROUTE}/0/q"))) qlog_len_2 = len(list(LogReader([f"{TEST_ROUTE}/0/q", f"{TEST_ROUTE}/0/q"]))) - self.assertEqual(qlog_len*2, qlog_len_2) + self.assertEqual(qlog_len * 2, qlog_len_2) @pytest.mark.slow @mock.patch("openpilot.tools.lib.logreader._LogFileReader") @@ -137,6 +181,41 @@ class TestLogReader(unittest.TestCase): lr = LogReader(f"{TEST_ROUTE}/0:4") self.assertEqual(len(lr.run_across_segments(4, noop)), len(list(lr))) + @pytest.mark.slow + def test_auto_mode(self): + lr = LogReader(f"{TEST_ROUTE}/0/q") + qlog_len = len(list(lr)) + with mock.patch("openpilot.tools.lib.route.Route.log_paths") as log_paths_mock: + log_paths_mock.return_value = [None] * NUM_SEGS + # Should fall back to qlogs since rlogs are not available + + with self.subTest("interactive_yes"): + with mock.patch("sys.stdin", new=io.StringIO("y\n")): + lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, default_source=comma_api_source) + log_len = len(list(lr)) + self.assertEqual(qlog_len, log_len) + + with self.subTest("interactive_no"): + with mock.patch("sys.stdin", new=io.StringIO("n\n")): + with self.assertRaises(AssertionError): + lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, default_source=comma_api_source) + + with self.subTest("non_interactive"): + lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO, default_source=comma_api_source) + log_len = len(list(lr)) + self.assertEqual(qlog_len, log_len) + + @parameterized.expand([(True,), (False,)]) + @pytest.mark.slow + def test_auto_source_scenarios(self, is_internal): + lr = LogReader(QLOG_FILE) + qlog_len = len(list(lr)) + + with setup_source_scenario(is_internal=is_internal): + lr = LogReader(f"{TEST_ROUTE}/0/q") + log_len = len(list(lr)) + self.assertEqual(qlog_len, log_len) + if __name__ == "__main__": unittest.main() diff --git a/tools/lib/url_file.py b/tools/lib/url_file.py index be9c815c93..2d3d14ce8a 100644 --- a/tools/lib/url_file.py +++ b/tools/lib/url_file.py @@ -1,12 +1,11 @@ import logging import os +import socket import time -import threading from hashlib import sha256 -from urllib3 import PoolManager +from urllib3 import PoolManager, Retry +from urllib3.response import BaseHTTPResponse from urllib3.util import Timeout -from tenacity import retry, wait_random_exponential, stop_after_attempt -from typing import Optional from openpilot.common.file_helpers import atomic_write_in_dir from openpilot.system.hardware.hw import Paths @@ -16,7 +15,7 @@ CHUNK_SIZE = 1000 * K logging.getLogger("urllib3").setLevel(logging.WARNING) -def hash_256(link): +def hash_256(link: str) -> str: hsh = str(sha256((link.split("?")[0]).encode('utf-8')).hexdigest()) return hsh @@ -26,16 +25,25 @@ class URLFileException(Exception): class URLFile: - _pid: Optional[int] = None - _pool_manager: Optional[PoolManager] = None - _pool_manager_lock = threading.Lock() + _pool_manager: PoolManager|None = None - def __init__(self, url, debug=False, cache=None): - self._pool_manager = None + @staticmethod + def reset() -> None: + URLFile._pool_manager = None + + @staticmethod + def pool_manager() -> PoolManager: + if URLFile._pool_manager is None: + socket_options = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),] + retries = Retry(total=5, backoff_factor=0.5, status_forcelist=[409, 429, 503, 504]) + URLFile._pool_manager = PoolManager(num_pools=10, maxsize=100, socket_options=socket_options, retries=retries) + return URLFile._pool_manager + + def __init__(self, url: str, timeout: int=10, debug: bool=False, cache: bool|None=None): self._url = url + self._timeout = Timeout(connect=timeout, read=timeout) self._pos = 0 - self._length = None - self._local_file = None + self._length: int|None = None self._debug = debug # True by default, false if FILEREADER_CACHE is defined, but can be overwritten by the cache input self._force_download = not int(os.environ.get("FILEREADER_CACHE", "0")) @@ -48,32 +56,20 @@ class URLFile: def __enter__(self): return self - def __exit__(self, exc_type, exc_value, traceback): - if self._local_file is not None: - os.remove(self._local_file.name) - self._local_file.close() - self._local_file = None - - def _http_client(self) -> PoolManager: - if self._pool_manager is None: - pid = os.getpid() - with URLFile._pool_manager_lock: - if URLFile._pid != pid or URLFile._pool_manager is None: # unsafe to share after fork - URLFile._pid = pid - URLFile._pool_manager = PoolManager(num_pools=10, maxsize=10) - self._pool_manager = URLFile._pool_manager - return self._pool_manager - - @retry(wait=wait_random_exponential(multiplier=1, max=5), stop=stop_after_attempt(3), reraise=True) - def get_length_online(self): - timeout = Timeout(connect=50.0, read=500.0) - response = self._http_client().request('HEAD', self._url, timeout=timeout, preload_content=False) + def __exit__(self, exc_type, exc_value, traceback) -> None: + pass + + def _request(self, method: str, url: str, headers: dict[str, str]|None=None) -> BaseHTTPResponse: + return URLFile.pool_manager().request(method, url, timeout=self._timeout, headers=headers) + + def get_length_online(self) -> int: + response = self._request('HEAD', self._url) if not (200 <= response.status <= 299): return -1 length = response.headers.get('content-length', 0) return int(length) - def get_length(self): + def get_length(self) -> int: if self._length is not None: return self._length @@ -90,7 +86,7 @@ class URLFile: file_length.write(str(self._length)) return self._length - def read(self, ll=None): + def read(self, ll: int|None=None) -> bytes: if self._force_download: return self.read_aux(ll=ll) @@ -122,10 +118,9 @@ class URLFile: self._pos = file_end return response - @retry(wait=wait_random_exponential(multiplier=1, max=5), stop=stop_after_attempt(3), reraise=True) - def read_aux(self, ll=None): + def read_aux(self, ll: int|None=None) -> bytes: download_range = False - headers = {'Connection': 'keep-alive'} + headers = {} if self._pos != 0 or ll is not None: if ll is None: end = self.get_length() - 1 @@ -139,8 +134,7 @@ class URLFile: if self._debug: t1 = time.time() - timeout = Timeout(connect=50.0, read=500.0) - response = self._http_client().request('GET', self._url, timeout=timeout, preload_content=False, headers=headers) + response = self._request('GET', self._url, headers=headers) ret = response.data if self._debug: @@ -159,9 +153,12 @@ class URLFile: self._pos += len(ret) return ret - def seek(self, pos): + def seek(self, pos:int) -> None: self._pos = pos @property - def name(self): + def name(self) -> str: return self._url + + +os.register_at_fork(after_in_child=URLFile.reset) diff --git a/tools/lib/vidindex.py b/tools/lib/vidindex.py index 8156faba6b..f2e4e9ca45 100755 --- a/tools/lib/vidindex.py +++ b/tools/lib/vidindex.py @@ -3,7 +3,6 @@ import argparse import os import struct from enum import IntEnum -from typing import Tuple from openpilot.tools.lib.filereader import FileReader @@ -120,7 +119,7 @@ HEVC_CODED_SLICE_SEGMENT_NAL_UNITS = ( class VideoFileInvalid(Exception): pass -def get_ue(dat: bytes, start_idx: int, skip_bits: int) -> Tuple[int, int]: +def get_ue(dat: bytes, start_idx: int, skip_bits: int) -> tuple[int, int]: prefix_val = 0 prefix_len = 0 suffix_val = 0 @@ -184,7 +183,7 @@ def get_hevc_nal_unit_type(dat: bytes, nal_unit_start: int) -> HevcNalUnitType: print(" nal_unit_type:", nal_unit_type.name, f"({nal_unit_type.value})") return nal_unit_type -def get_hevc_slice_type(dat: bytes, nal_unit_start: int, nal_unit_type: HevcNalUnitType) -> Tuple[int, bool]: +def get_hevc_slice_type(dat: bytes, nal_unit_start: int, nal_unit_type: HevcNalUnitType) -> tuple[int, bool]: # 7.3.2.9 Slice segment layer RBSP syntax # slice_segment_layer_rbsp( ) { # slice_segment_header( ) @@ -259,7 +258,7 @@ def get_hevc_slice_type(dat: bytes, nal_unit_start: int, nal_unit_type: HevcNalU raise VideoFileInvalid("slice_type must be 0, 1, or 2") return slice_type, is_first_slice -def hevc_index(hevc_file_name: str, allow_corrupt: bool=False) -> Tuple[list, int, bytes]: +def hevc_index(hevc_file_name: str, allow_corrupt: bool=False) -> tuple[list, int, bytes]: with FileReader(hevc_file_name) as f: dat = f.read() diff --git a/tools/plotjuggler/juggle.py b/tools/plotjuggler/juggle.py index cc21095414..dc94062801 100755 --- a/tools/plotjuggler/juggle.py +++ b/tools/plotjuggler/juggle.py @@ -73,7 +73,7 @@ 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, can, layout, dbc=None): - sr = LogReader(route_or_segment_name, default_mode=ReadMode.AUTO_INTERACIVE) + sr = LogReader(route_or_segment_name, default_mode=ReadMode.AUTO_INTERACTIVE) all_data = sr.run_across_segments(24, partial(process, can)) diff --git a/tools/plotjuggler/layouts/system_lag_debug.xml b/tools/plotjuggler/layouts/system_lag_debug.xml index b1699348cc..a90bba0e27 100644 --- a/tools/plotjuggler/layouts/system_lag_debug.xml +++ b/tools/plotjuggler/layouts/system_lag_debug.xml @@ -16,7 +16,6 @@ - diff --git a/tools/replay/can_replay.py b/tools/replay/can_replay.py index ef6864f110..c597df1f66 100755 --- a/tools/replay/can_replay.py +++ b/tools/replay/can_replay.py @@ -10,45 +10,49 @@ os.environ['FILEREADER_CACHE'] = '1' from openpilot.common.realtime import config_realtime_process, Ratekeeper, DT_CTRL from openpilot.selfdrive.boardd.boardd import can_capnp_to_can_list from openpilot.tools.lib.logreader import LogReader -from panda import Panda, PandaJungle +from panda import PandaJungle + +# set both to cycle power or ignition +PWR_ON = int(os.getenv("PWR_ON", "0")) +PWR_OFF = int(os.getenv("PWR_OFF", "0")) +IGN_ON = int(os.getenv("ON", "0")) +IGN_OFF = int(os.getenv("OFF", "0")) +ENABLE_IGN = IGN_ON > 0 and IGN_OFF > 0 +ENABLE_PWR = PWR_ON > 0 and PWR_OFF > 0 + def send_thread(s, flock): - if "Jungle" in str(type(s)): - if "FLASH" in os.environ: - with flock: - s.flash() - - for i in [0, 1, 2, 3, 0xFFFF]: - s.can_clear(i) - s.set_can_speed_kbps(i, 500) - s.set_can_data_speed_kbps(i, 500) - s.set_ignition(False) - time.sleep(5) - s.set_ignition(True) - s.set_panda_power(True) - else: - s.set_safety_mode(Panda.SAFETY_ALLOUTPUT) + if "FLASH" in os.environ: + with flock: + s.flash() + + for i in [0, 1, 2, 3, 0xFFFF]: + s.can_clear(i) + s.set_can_speed_kbps(i, 500) + s.set_can_data_speed_kbps(i, 500) + s.set_ignition(False) + time.sleep(5) + s.set_ignition(True) + s.set_panda_power(True) s.set_can_loopback(False) - idx = 0 - ign = True rk = Ratekeeper(1 / DT_CTRL, print_delay_threshold=None) while True: - # handle ignition cycling + # handle cycling + if ENABLE_PWR: + i = (rk.frame*DT_CTRL) % (PWR_ON + PWR_OFF) < PWR_ON + s.set_panda_power(i) if ENABLE_IGN: i = (rk.frame*DT_CTRL) % (IGN_ON + IGN_OFF) < IGN_ON - if i != ign: - ign = i - s.set_ignition(ign) + s.set_ignition(i) - snd = CAN_MSGS[idx] + snd = CAN_MSGS[rk.frame % len(CAN_MSGS)] snd = list(filter(lambda x: x[-1] <= 2, snd)) try: s.can_send_many(snd) except usb1.USBErrorTimeout: # timeout is fine, just means the CAN TX buffer is full pass - idx = (idx + 1) % len(CAN_MSGS) # Drain panda message buffer s.can_recv() @@ -98,10 +102,8 @@ if __name__ == "__main__": print("Finished loading...") - # set both to cycle ignition - IGN_ON = int(os.getenv("ON", "0")) - IGN_OFF = int(os.getenv("OFF", "0")) - ENABLE_IGN = IGN_ON > 0 and IGN_OFF > 0 + if ENABLE_PWR: + print(f"Cycling power: on for {PWR_ON}s, off for {PWR_OFF}s") if ENABLE_IGN: print(f"Cycling ignition: on for {IGN_ON}s, off for {IGN_OFF}s") diff --git a/tools/replay/lib/ui_helpers.py b/tools/replay/lib/ui_helpers.py index e350b89bac..23f3563084 100644 --- a/tools/replay/lib/ui_helpers.py +++ b/tools/replay/lib/ui_helpers.py @@ -1,5 +1,5 @@ import itertools -from typing import Any, Dict, Tuple +from typing import Any import matplotlib.pyplot as plt import numpy as np @@ -84,7 +84,7 @@ class Calibration: return pts / self.zoom -_COLOR_CACHE : Dict[Tuple[int, int, int], Any] = {} +_COLOR_CACHE : dict[tuple[int, int, int], Any] = {} def find_color(lidar_surface, color): if color in _COLOR_CACHE: return _COLOR_CACHE[color] diff --git a/tools/replay/ui.py b/tools/replay/ui.py index 7c95a75f8b..be80166e76 100755 --- a/tools/replay/ui.py +++ b/tools/replay/ui.py @@ -154,10 +154,10 @@ def ui_thread(addr): if len(sm['longitudinalPlan'].accels): plot_arr[-1, name_to_arr_idx['a_target']] = sm['longitudinalPlan'].accels[0] - if sm.rcv_frame['modelV2']: + if sm.recv_frame['modelV2']: plot_model(sm['modelV2'], img, calibration, top_down) - if sm.rcv_frame['radarState']: + if sm.recv_frame['radarState']: plot_lead(sm['radarState'], top_down) # draw all radar points diff --git a/tools/sim/Dockerfile.sim b/tools/sim/Dockerfile.sim index a183002589..cb573b33f2 100644 --- a/tools/sim/Dockerfile.sim +++ b/tools/sim/Dockerfile.sim @@ -1,4 +1,4 @@ -FROM ghcr.io/commaai/openpilot-base-cl:latest +FROM ghcr.io/commaai/openpilot-base:latest RUN apt-get update && apt-get install -y --no-install-recommends \ tmux \ diff --git a/tools/sim/bridge/common.py b/tools/sim/bridge/common.py index 91ab0b6f07..10e2a055a3 100644 --- a/tools/sim/bridge/common.py +++ b/tools/sim/bridge/common.py @@ -4,7 +4,6 @@ import functools from multiprocessing import Process, Queue, Value from abc import ABC, abstractmethod -from typing import Optional from openpilot.common.params import Params from openpilot.common.numpy_fast import clip @@ -44,7 +43,7 @@ class SimulatorBridge(ABC): self._exit = threading.Event() self.simulator_state = SimulatorState() - self.world: Optional[World] = None + self.world: World | None = None self.past_startup_engaged = False diff --git a/tools/sim/bridge/metadrive/metadrive_bridge.py b/tools/sim/bridge/metadrive/metadrive_bridge.py index 1b1e5ffea6..c94fca2b95 100644 --- a/tools/sim/bridge/metadrive/metadrive_bridge.py +++ b/tools/sim/bridge/metadrive/metadrive_bridge.py @@ -30,14 +30,14 @@ class CopyRamRGBCamera(RGBCamera): class RGBCameraWide(CopyRamRGBCamera): def __init__(self, *args, **kwargs): - super(RGBCameraWide, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) lens = self.get_lens() lens.setFov(120) lens.setNear(0.1) class RGBCameraRoad(CopyRamRGBCamera): def __init__(self, *args, **kwargs): - super(RGBCameraRoad, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) lens = self.get_lens() lens.setFov(40) lens.setNear(0.1) @@ -85,7 +85,7 @@ class MetaDriveBridge(SimulatorBridge): def __init__(self, dual_camera, high_quality): self.should_render = False - super(MetaDriveBridge, self).__init__(dual_camera, high_quality) + super().__init__(dual_camera, high_quality) def spawn_world(self): sensors = { diff --git a/tools/sim/build_container.sh b/tools/sim/build_container.sh index 81afb82d83..451277d590 100755 --- a/tools/sim/build_container.sh +++ b/tools/sim/build_container.sh @@ -3,7 +3,7 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $DIR/../../ -docker pull ghcr.io/commaai/openpilot-base-cl:latest +docker pull ghcr.io/commaai/openpilot-base:latest docker build \ --cache-from ghcr.io/commaai/openpilot-sim:latest \ -t ghcr.io/commaai/openpilot-sim:latest \ diff --git a/tools/sim/lib/manual_ctrl.py b/tools/sim/lib/manual_ctrl.py index 5e826e7baa..8a72296538 100755 --- a/tools/sim/lib/manual_ctrl.py +++ b/tools/sim/lib/manual_ctrl.py @@ -4,7 +4,7 @@ import array import os import struct from fcntl import ioctl -from typing import NoReturn, Dict, List +from typing import NoReturn # Iterate over the joystick devices. print('Available devices:') @@ -13,8 +13,8 @@ for fn in os.listdir('/dev/input'): print(f' /dev/input/{fn}') # We'll store the states here. -axis_states: Dict[str, float] = {} -button_states: Dict[str, float] = {} +axis_states: dict[str, float] = {} +button_states: dict[str, float] = {} # These constants were borrowed from linux/input.h axis_names = { @@ -88,8 +88,8 @@ button_names = { 0x2c3 : 'dpad_down', } -axis_name_list: List[str] = [] -button_name_list: List[str] = [] +axis_name_list: list[str] = [] +button_name_list: list[str] = [] def wheel_poll_thread(q: 'Queue[str]') -> NoReturn: # Open the joystick device. diff --git a/tools/sim/lib/simulated_car.py b/tools/sim/lib/simulated_car.py index 85b4ac2387..f6319dd819 100644 --- a/tools/sim/lib/simulated_car.py +++ b/tools/sim/lib/simulated_car.py @@ -2,6 +2,7 @@ import cereal.messaging as messaging from opendbc.can.packer import CANPacker from opendbc.can.parser import CANParser +from openpilot.common.params import Params from openpilot.selfdrive.boardd.boardd_api_impl import can_list_to_can_capnp from openpilot.selfdrive.car import crc8_pedal from openpilot.tools.sim.lib.common import SimulatorState @@ -18,6 +19,8 @@ class SimulatedCar: self.sm = messaging.SubMaster(['carControl', 'controlsState', 'carParams']) self.cp = self.get_car_can_parser() self.idx = 0 + self.params = Params() + self.obd_multiplexing = False @staticmethod def get_car_can_parser(): @@ -100,6 +103,11 @@ class SimulatedCar: def send_panda_state(self, simulator_state): self.sm.update(0) + + if self.params.get_bool("ObdMultiplexingEnabled") != self.obd_multiplexing: + self.obd_multiplexing = not self.obd_multiplexing + self.params.put_bool("ObdMultiplexingChanged", True) + dat = messaging.new_message('pandaStates', 1) dat.valid = True dat.pandaStates[0] = { diff --git a/tools/sim/lib/simulated_sensors.py b/tools/sim/lib/simulated_sensors.py index ebbcc6c9e8..df6a0aeeff 100644 --- a/tools/sim/lib/simulated_sensors.py +++ b/tools/sim/lib/simulated_sensors.py @@ -3,7 +3,6 @@ import time from cereal import log import cereal.messaging as messaging -from openpilot.common.params import Params from openpilot.common.realtime import DT_DMON from openpilot.tools.sim.lib.camerad import Camerad @@ -80,7 +79,6 @@ class SimulatedSensors: 'current': 5678, 'fanSpeedRpm': 1000 } - Params().put_bool("ObdMultiplexingEnabled", False) self.pm.send('peripheralState', dat) def send_fake_driver_monitoring(self):