diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b1a14076ea..47eb6c216f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,6 +8,7 @@ body: value: > Before creating a **bug report**, please check the following: * If the issue likely only affects your car model or make, go back and open a **car bug report** instead. + * If the issue is related to the driving or driver monitoring models, you should open a [discussion](https://github.com/commaai/openpilot/discussions/categories/model-feedback) instead. * Ensure you're running the latest openpilot release. * Ensure you're using officially supported hardware. Issues running on PCs have a different issue template. * Ensure there isn't an existing issue for your bug. If there is, leave a comment on the existing issue. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2c2deb17ba..45a8af0aaf 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,11 @@ blank_issues_enabled: false contact_links: + - name: Report model bugs + url: https://github.com/commaai/openpilot/discussions/categories/model-feedback + about: Provide feedback for the driving or driver monitoring models - name: Discussions url: https://github.com/commaai/openpilot/discussions - about: For questions and discussion about openpilot + about: For questions and general discussion about openpilot - name: Community Wiki url: https://github.com/commaai/openpilot/wiki about: Check out our community wiki diff --git a/.github/workflows/prebuilt.yaml b/.github/workflows/prebuilt.yaml index b659d4ceee..c8b4c51e38 100644 --- a/.github/workflows/prebuilt.yaml +++ b/.github/workflows/prebuilt.yaml @@ -2,7 +2,6 @@ name: prebuilt on: schedule: - cron: '0 * * * *' - workflow_dispatch: env: @@ -11,9 +10,7 @@ env: DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} BUILD: | - docker pull $(grep -iohP '(?<=^from)\s+\S+' Dockerfile.openpilot_base) || true - docker pull $DOCKER_REGISTRY/$BASE_IMAGE:latest || true - docker build --cache-from $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $BASE_IMAGE:latest -f Dockerfile.openpilot_base . + DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $BASE_IMAGE:latest -f Dockerfile.openpilot_base . jobs: build_prebuilt: @@ -37,8 +34,7 @@ jobs: - name: Build Docker image run: | eval "$BUILD" - docker pull $DOCKER_REGISTRY/$IMAGE_NAME:latest || true - docker build --cache-from $DOCKER_REGISTRY/$IMAGE_NAME:latest -t $DOCKER_REGISTRY/$IMAGE_NAME:latest -f Dockerfile.openpilot . + DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/$IMAGE_NAME:latest -t $DOCKER_REGISTRY/$IMAGE_NAME:latest -f Dockerfile.openpilot . - name: Push to container registry run: | $DOCKER_LOGIN diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 9606c05631..d84779103b 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -1,10 +1,15 @@ name: selfdrive + on: push: branches-ignore: - 'testing-closet*' pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref != 'refs/heads/master' && github.ref || github.run_id }}-${{ github.event_name }} + cancel-in-progress: true + env: BASE_IMAGE: openpilot-base CL_BASE_IMAGE: openpilot-base-cl @@ -13,15 +18,12 @@ env: DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} BUILD: | - docker pull $(grep -iohP '(?<=^from)\s+\S+' Dockerfile.openpilot_base) || true - docker pull $DOCKER_REGISTRY/$BASE_IMAGE:latest || true - docker build --cache-from $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $BASE_IMAGE:latest -f Dockerfile.openpilot_base . + DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $BASE_IMAGE:latest -f Dockerfile.openpilot_base . RUN: docker run --shm-size 1G -v $PWD:/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 /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache -v /tmp/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/sh -c BUILD_CL: | - docker pull $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest || true - docker build --cache-from $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $CL_BASE_IMAGE:latest -f Dockerfile.openpilot_base_cl . + DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $CL_BASE_IMAGE:latest -f Dockerfile.openpilot_base_cl . RUN_CL: docker run --shm-size 1G -v $PWD:/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 /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache -v /tmp/openpilot_cache:/tmp/openpilot_cache $CL_BASE_IMAGE /bin/sh -c UNIT_TEST: coverage run --append -m unittest discover @@ -36,13 +38,10 @@ jobs: steps: - uses: actions/checkout@v3 with: - fetch-depth: 0 submodules: true - name: Build devel run: TARGET_DIR=$STRIPPED_DIR release/build_devel.sh - uses: ./.github/workflows/setup - with: - save-cache: true - name: Check submodules if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot' run: release/check-submodules.sh @@ -62,23 +61,25 @@ jobs: cp .pylintrc $STRIPPED_DIR cp mypy.ini $STRIPPED_DIR cd $STRIPPED_DIR - ${{ env.RUN }} "pre-commit run --all" + ${{ env.RUN }} "SKIP=test_translations pre-commit run --all" build_all: name: build all runs-on: ubuntu-20.04 - timeout-minutes: 50 + timeout-minutes: 30 steps: - uses: actions/checkout@v3 with: submodules: true - uses: ./.github/workflows/setup + with: + save-cache: true - name: Build openpilot with all flags run: ${{ env.RUN }} "scons -j$(nproc) --extras && release/check-dirty.sh" - name: Cleanup scons cache run: | ${{ env.RUN }} "rm -rf /tmp/scons_cache/* && \ - scons -j$(nproc) --extras --cache-populate" + scons -j$(nproc) --cache-populate" #build_mac: # name: build macos @@ -140,7 +141,7 @@ jobs: docker_push: name: docker push runs-on: ubuntu-20.04 - timeout-minutes: 50 + timeout-minutes: 22 if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot' needs: static_analysis # hack to ensure slow tests run first since this and static_analysis are fast steps: @@ -149,12 +150,14 @@ jobs: submodules: true - name: Build Docker image run: eval "$BUILD" + timeout-minutes: 13 - name: Push to container registry run: | $DOCKER_LOGIN docker push $DOCKER_REGISTRY/$BASE_IMAGE:latest - name: Build CL Docker image run: eval "$BUILD_CL" + timeout-minutes: 4 - name: Push to container registry run: | $DOCKER_LOGIN @@ -163,7 +166,7 @@ jobs: static_analysis: name: static analysis runs-on: ubuntu-20.04 - timeout-minutes: 50 + timeout-minutes: 20 steps: - uses: actions/checkout@v3 with: @@ -171,21 +174,23 @@ jobs: - name: Build Docker image run: eval "$BUILD" - name: pre-commit + timeout-minutes: 5 run: ${{ env.RUN }} "pre-commit run --all" valgrind: name: valgrind runs-on: ubuntu-20.04 - timeout-minutes: 50 + timeout-minutes: 20 steps: - uses: actions/checkout@v3 with: submodules: true - uses: ./.github/workflows/setup + - name: Build openpilot + run: ${{ env.RUN }} "scons -j$(nproc)" - name: Run valgrind run: | - ${{ env.RUN }} "scons -j$(nproc) && \ - python selfdrive/test/test_valgrind_replay.py" + ${{ env.RUN }} "python selfdrive/test/test_valgrind_replay.py" - name: Print logs if: always() run: cat selfdrive/test/valgrind_logs.txt @@ -193,16 +198,18 @@ jobs: unit_tests: name: unit tests runs-on: ubuntu-20.04 - timeout-minutes: 50 + timeout-minutes: 30 steps: - uses: actions/checkout@v3 with: submodules: true - uses: ./.github/workflows/setup + - name: Build openpilot + run: ${{ env.RUN }} "scons -j$(nproc)" - name: Run unit tests + timeout-minutes: 15 run: | ${{ env.RUN }} "export SKIP_LONG_TESTS=1 && \ - scons -j$(nproc) && \ $UNIT_TEST common && \ $UNIT_TEST opendbc/can && \ $UNIT_TEST selfdrive/boardd && \ @@ -215,7 +222,6 @@ jobs: $UNIT_TEST selfdrive/athena && \ $UNIT_TEST selfdrive/thermald && \ $UNIT_TEST system/hardware/tici && \ - $UNIT_TEST selfdrive/modeld && \ $UNIT_TEST tools/lib/tests && \ ./selfdrive/ui/tests/create_test_translations.sh && \ QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \ @@ -234,7 +240,7 @@ jobs: process_replay: name: process replay runs-on: ubuntu-20.04 - timeout-minutes: 50 + timeout-minutes: 25 steps: - uses: actions/checkout@v3 with: @@ -246,10 +252,12 @@ jobs: with: path: /tmp/comma_download_cache key: proc-replay-${{ hashFiles('.github/workflows/selfdrive_tests.yaml', 'selfdrive/test/process_replay/ref_commit') }} + - name: Build openpilot + run: | + ${{ env.RUN }} "scons -j$(nproc)" - name: Run replay run: | - ${{ env.RUN }} "scons -j$(nproc) && \ - CI=1 coverage run selfdrive/test/process_replay/test_processes.py -j$(nproc) && \ + ${{ env.RUN }} "CI=1 coverage run selfdrive/test/process_replay/test_processes.py -j$(nproc) && \ coverage xml" - name: Print diff if: always() @@ -263,15 +271,14 @@ jobs: - name: Upload reference logs if: ${{ failure() && github.event_name == 'pull_request' && github.repository == 'commaai/openpilot' && env.AZURE_TOKEN != '' }} run: | - ${{ env.RUN }} "scons -j$(nproc) && \ - CI=1 AZURE_TOKEN='$AZURE_TOKEN' python selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only" + ${{ env.RUN }} "CI=1 AZURE_TOKEN='$AZURE_TOKEN' python selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only" - name: "Upload coverage to Codecov" uses: codecov/codecov-action@v2 - model_replay_onnx: - name: model replay onnx + test_modeld: + name: model tests runs-on: ubuntu-20.04 - timeout-minutes: 50 + timeout-minutes: 20 steps: - uses: actions/checkout@v3 with: @@ -280,29 +287,41 @@ jobs: - name: Build Docker image # Sim docker is needed to get the OpenCL drivers run: eval "$BUILD_CL" - - name: Run replay + - name: Build openpilot + run: | + ${{ env.RUN }} "scons -j$(nproc)" + - name: Run model replay with ONNX + timeout-minutes: 2 + run: | + ${{ env.RUN_CL }} "ONNXCPU=1 CI=1 coverage run selfdrive/test/process_replay/model_replay.py && \ + coverage xml" + - name: Run unit tests + timeout-minutes: 5 run: | - ${{ env.RUN_CL }} "scons -j$(nproc) && \ - ONNXCPU=1 CI=1 coverage run \ - selfdrive/test/process_replay/model_replay.py -j$(nproc) && \ + ${{ env.RUN_CL }} "$UNIT_TEST selfdrive/modeld && \ coverage xml" + - name: "Upload coverage to Codecov" + uses: codecov/codecov-action@v2 test_longitudinal: name: longitudinal runs-on: ubuntu-20.04 - timeout-minutes: 50 + timeout-minutes: 20 steps: - uses: actions/checkout@v3 with: submodules: true - uses: ./.github/workflows/setup + - name: Build openpilot + run: | + ${{ env.RUN }} "scons -j$(nproc)" - name: Test longitudinal run: | ${{ env.RUN }} "mkdir -p selfdrive/test/out && \ - scons -j$(nproc) && \ cd selfdrive/test/longitudinal_maneuvers && \ coverage run ./test_longitudinal.py && \ coverage xml" + timeout-minutes: 2 - name: "Upload coverage to Codecov" uses: codecov/codecov-action@v2 - uses: actions/upload-artifact@v2 @@ -315,7 +334,7 @@ jobs: test_cars: name: cars runs-on: ubuntu-20.04 - timeout-minutes: 50 + timeout-minutes: 20 strategy: fail-fast: false matrix: @@ -331,10 +350,12 @@ jobs: with: path: /tmp/comma_download_cache key: car_models-${{ hashFiles('selfdrive/car/tests/test_models.py', 'selfdrive/car/tests/routes.py') }}-${{ matrix.job }} + - name: Build openpilot + run: ${{ env.RUN }} "scons -j$(nproc)" - name: Test car models + timeout-minutes: 12 run: | - ${{ env.RUN }} "scons -j$(nproc) && \ - coverage run -m pytest selfdrive/car/tests/test_models.py && \ + ${{ env.RUN }} "coverage run -m pytest selfdrive/car/tests/test_models.py && \ coverage xml && \ chmod -R 777 /tmp/comma_download_cache" env: @@ -343,29 +364,10 @@ jobs: - name: "Upload coverage to Codecov" uses: codecov/codecov-action@v2 - docs: - name: build docs - runs-on: ubuntu-20.04 - timeout-minutes: 50 - steps: - - uses: actions/checkout@v3 - with: - submodules: true - - name: Build docker container - run: | - docker pull $DOCKER_REGISTRY/$BASE_IMAGE:latest || true - docker pull $DOCKER_REGISTRY/openpilot-docs:latest || true - DOCKER_BUILDKIT=1 docker build --cache-from $DOCKER_REGISTRY/openpilot-docs:latest -t $DOCKER_REGISTRY/openpilot-docs:latest -f docs/docker/Dockerfile . - - name: Push docker container - if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot' - run: | - $DOCKER_LOGIN - docker push $DOCKER_REGISTRY/openpilot-docs:latest - car_docs_diff: - name: comment on PR with car docs diff + name: PR comments runs-on: ubuntu-20.04 - timeout-minutes: 50 + timeout-minutes: 20 if: github.event_name == 'pull_request' steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/setup/action.yaml b/.github/workflows/setup/action.yaml index 79c4c890ab..186a9d9095 100644 --- a/.github/workflows/setup/action.yaml +++ b/.github/workflows/setup/action.yaml @@ -15,9 +15,9 @@ runs: # build cache - id: date shell: bash - run: echo "::set-output name=date::$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d')" + run: echo "CACHE_COMMIT_DATE=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d-%H:%M')" >> $GITHUB_ENV - shell: bash - run: echo "${{ steps.date.outputs.date }}" + run: echo "$CACHE_COMMIT_DATE" - shell: bash run: echo "CACHE_SKIP_SAVE=true" >> $GITHUB_ENV if: github.ref != 'refs/heads/master' || inputs.save-cache == 'false' @@ -27,9 +27,9 @@ runs: uses: actions/cache@03e00da99d75a2204924908e1cca7902cafce66b with: path: /tmp/scons_cache - key: scons-${{ steps.date.outputs.date }}-${{ github.sha }} + key: scons-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} restore-keys: | - scons-${{ steps.date.outputs.date }}- + scons-${{ env.CACHE_COMMIT_DATE }}- scons- # build our docker image diff --git a/.github/workflows/tools_tests.yaml b/.github/workflows/tools_tests.yaml index 173e208384..549a2f4195 100644 --- a/.github/workflows/tools_tests.yaml +++ b/.github/workflows/tools_tests.yaml @@ -1,8 +1,15 @@ name: tools + on: push: + branches-ignore: + - 'testing-closet*' pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref != 'refs/heads/master' && github.ref || github.run_id }}-${{ github.event_name }} + cancel-in-progress: true + env: BASE_IMAGE: openpilot-base CL_BASE_IMAGE: openpilot-base-cl @@ -10,21 +17,20 @@ env: DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} BUILD: | - docker pull $(grep -iohP '(?<=^from)\s+\S+' Dockerfile.openpilot_base) || true - docker pull $DOCKER_REGISTRY/$BASE_IMAGE:latest || true - docker build --cache-from $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $BASE_IMAGE:latest -f Dockerfile.openpilot_base . + DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $BASE_IMAGE:latest -f Dockerfile.openpilot_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 /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache -v /tmp/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/sh -c + BUILD_CL: | - docker pull $(grep -iohP '(?<=^from)\s+\S+' Dockerfile.openpilot_base_cl) || true - docker pull $DOCKER_REGISTRY/$BASE_IMAGE:latest || true - docker build --cache-from $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $CL_BASE_IMAGE:latest -f Dockerfile.openpilot_base_cl . - RUN: docker run --shm-size 1G -v $PWD:/tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e \ - GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/comma_download_cache:/tmp/comma_download_cache $BASE_IMAGE /bin/sh -c + DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $CL_BASE_IMAGE:latest -f Dockerfile.openpilot_base_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 /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache -v /tmp/openpilot_cache:/tmp/openpilot_cache $CL_BASE_IMAGE /bin/sh -c + jobs: plotjuggler: name: plotjuggler runs-on: ubuntu-20.04 - timeout-minutes: 30 + timeout-minutes: 20 steps: - uses: actions/checkout@v3 with: @@ -32,6 +38,7 @@ jobs: - name: Build Docker image run: eval "$BUILD" - name: Unit test + timeout-minutes: 2 run: | ${{ env.RUN }} "scons -j$(nproc) --directory=/tmp/openpilot/cereal && \ apt-get update && \ @@ -42,7 +49,7 @@ jobs: simulator: name: simulator runs-on: ubuntu-20.04 - timeout-minutes: 50 + timeout-minutes: 30 env: IMAGE_NAME: openpilot-sim if: github.repository == 'commaai/openpilot' @@ -56,12 +63,29 @@ jobs: run: eval "$BUILD" - name: Build base cl image run: eval "$BUILD_CL" - - name: Pull latest simulator image - run: docker pull $DOCKER_REGISTRY/$IMAGE_NAME:latest || true - name: Build simulator image - run: docker build --cache-from $DOCKER_REGISTRY/$IMAGE_NAME:latest -t $DOCKER_REGISTRY/$IMAGE_NAME:latest -f tools/sim/Dockerfile.sim . + run: DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/$IMAGE_NAME:latest -t $DOCKER_REGISTRY/$IMAGE_NAME:latest -f tools/sim/Dockerfile.sim . - name: Push to container registry if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot' run: | $DOCKER_LOGIN docker push $DOCKER_REGISTRY/$IMAGE_NAME:latest + + docs: + name: build docs + runs-on: ubuntu-20.04 + timeout-minutes: 25 + steps: + - uses: actions/checkout@v3 + with: + submodules: true + - name: Build docker container + run: | + DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/openpilot-docs:latest -t $DOCKER_REGISTRY/openpilot-docs:latest -f docs/docker/Dockerfile . + - name: Push docker container + if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot' + run: | + $DOCKER_LOGIN + docker push $DOCKER_REGISTRY/openpilot-docs:latest + + diff --git a/.gitmodules b/.gitmodules index 26f93ef164..544c95c943 100644 --- a/.gitmodules +++ b/.gitmodules @@ -18,4 +18,4 @@ url = ../../commaai/body.git [submodule "tinygrad"] path = tinygrad_repo - url = https://github.com/geohot/tinygrad.git + url = ../../commaai/tinygrad.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 347216f2fb..85c24b911b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,18 +24,14 @@ 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 - --builtins clear,rare,informal,usage,code,names,en-GB_to_en-US -- repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.931 +- repo: local hooks: - id: mypy - exclude: '^(pyextra/)|(cereal/)|(rednose/)|(panda/)|(laika/)|(opendbc/)|(laika_repo/)|(rednose_repo/)/|(tinygrad/)|(tinygrad_repo/)' - additional_dependencies: ['types-PyYAML', 'lxml', 'numpy', 'types-atomicwrites', 'types-pycurl', 'types-requests', 'types-certifi'] - args: - - --warn-redundant-casts - - --warn-return-any - - --warn-unreachable - - --warn-unused-ignores - #- --html-report=/home/batman/openpilot + name: mypy + entry: mypy + language: system + types: [python] + exclude: '^(pyextra/)|(cereal/)|(opendbc/)|(panda/)|(laika/)|(laika_repo/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(xx/)' - repo: https://github.com/PyCQA/flake8 rev: 4.0.1 hooks: @@ -57,6 +53,7 @@ repos: types: [python] exclude: '^(pyextra/)|(cereal/)|(rednose/)|(panda/)|(laika/)|(laika_repo/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)' args: + - -j0 - -rn - -sn - --rcfile=.pylintrc @@ -74,3 +71,10 @@ repos: - --quiet - --force - -j8 +- repo: local + hooks: + - id: test_translations + name: test translations + entry: selfdrive/ui/tests/test_translations.py + language: script + pass_filenames: false diff --git a/Jenkinsfile b/Jenkinsfile index c4038090e1..b9b9eda667 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -127,7 +127,7 @@ pipeline { steps { phone_steps("tici2", [ ["build", "cd selfdrive/manager && ./build.py"], - //["test power draw", "python system/hardware/tici/test_power_draw.py"], + ["test power draw", "python system/hardware/tici/test_power_draw.py"], ["test boardd loopback", "python selfdrive/boardd/tests/test_boardd_loopback.py"], ["test loggerd", "python selfdrive/loggerd/tests/test_loggerd.py"], ["test encoder", "LD_LIBRARY_PATH=/usr/local/lib python selfdrive/loggerd/tests/test_encoder.py"], diff --git a/Pipfile b/Pipfile index 644fd2a90b..778cf8c33c 100644 --- a/Pipfile +++ b/Pipfile @@ -43,6 +43,12 @@ carla = {version = "==0.9.13", markers="platform_system != 'Darwin'"} ft4222 = "*" pandas = "*" tabulate = "*" +types-pyyaml = "*" +lxml = "*" +types-atomicwrites = "*" +types-pycurl = "*" +types-requests = "*" +types-certifi = "*" [packages] atomicwrites = "*" diff --git a/Pipfile.lock b/Pipfile.lock index e6f05fbcd4..d033a517cb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "adf64558652d394d9de8e45777f1a2f50ed1ac37b75664206e7957792832b5a4" + "sha256": "c406463198490fc40dcac3dd438c77cf36f6fe681793072e96fdecf089ff7639" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "astroid": { "hashes": [ - "sha256:396c88d0a58d7f8daadf730b2ce90838bf338c6752558db719ec6f99c18ec20e", - "sha256:d612609242996c4365aeb0345e61edba34363eaaba55f1c0addf6a98f073bef6" + "sha256:81f870105d892e73bf535da77a8261aa5bde838fa4ed12bb2f435291a098c581", + "sha256:997e0c735df60d4a4caff27080a3afc51f9bdd693d3572a4a0b7090b645c36c5" ], "markers": "python_full_version >= '3.7.2'", - "version": "==2.12.5" + "version": "==2.12.10" }, "atomicwrites": { "hashes": [ @@ -78,11 +78,11 @@ }, "certifi": { "hashes": [ - "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", - "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" + "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", + "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" ], "markers": "python_version >= '3.6'", - "version": "==2022.6.15" + "version": "==2022.9.24" }, "cffi": { "hashes": [ @@ -190,31 +190,35 @@ }, "cryptography": { "hashes": [ - "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59", - "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596", - "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3", - "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5", - "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab", - "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884", - "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82", - "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b", - "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441", - "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa", - "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d", - "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b", - "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a", - "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6", - "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157", - "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280", - "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282", - "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67", - "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8", - "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046", - "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327", - "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9" - ], - "index": "pypi", - "version": "==37.0.4" + "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a", + "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f", + "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0", + "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407", + "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7", + "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6", + "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153", + "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750", + "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad", + "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6", + "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b", + "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5", + "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a", + "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d", + "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d", + "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294", + "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0", + "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a", + "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac", + "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61", + "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013", + "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e", + "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb", + "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9", + "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd", + "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818" + ], + "index": "pypi", + "version": "==38.0.1" }, "cython": { "hashes": [ @@ -373,11 +377,11 @@ }, "idna": { "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], "markers": "python_version >= '3.5'", - "version": "==3.3" + "version": "==3.4" }, "importlib-metadata": { "hashes": [ @@ -400,7 +404,7 @@ "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" ], - "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", + "markers": "python_version < '4' and python_full_version >= '3.6.1'", "version": "==5.10.1" }, "itsdangerous": { @@ -593,37 +597,37 @@ }, "numpy": { "hashes": [ - "sha256:17e5226674f6ea79e14e3b91bfbc153fdf3ac13f5cc54ee7bc8fdbe820a32da0", - "sha256:2bd879d3ca4b6f39b7770829f73278b7c5e248c91d538aab1e506c628353e47f", - "sha256:4f41f5bf20d9a521f8cab3a34557cd77b6f205ab2116651f12959714494268b0", - "sha256:5593f67e66dea4e237f5af998d31a43e447786b2154ba1ad833676c788f37cde", - "sha256:5e28cd64624dc2354a349152599e55308eb6ca95a13ce6a7d5679ebff2962913", - "sha256:633679a472934b1c20a12ed0c9a6c9eb167fbb4cb89031939bfd03dd9dbc62b8", - "sha256:806970e69106556d1dd200e26647e9bee5e2b3f1814f9da104a943e8d548ca38", - "sha256:806cc25d5c43e240db709875e947076b2826f47c2c340a5a2f36da5bb10c58d6", - "sha256:8247f01c4721479e482cc2f9f7d973f3f47810cbc8c65e38fd1bbd3141cc9842", - "sha256:8ebf7e194b89bc66b78475bd3624d92980fca4e5bb86dda08d677d786fefc414", - "sha256:8ecb818231afe5f0f568c81f12ce50f2b828ff2b27487520d85eb44c71313b9e", - "sha256:8f9d84a24889ebb4c641a9b99e54adb8cab50972f0166a3abc14c3b93163f074", - "sha256:909c56c4d4341ec8315291a105169d8aae732cfb4c250fbc375a1efb7a844f8f", - "sha256:9b83d48e464f393d46e8dd8171687394d39bc5abfe2978896b77dc2604e8635d", - "sha256:ac987b35df8c2a2eab495ee206658117e9ce867acf3ccb376a19e83070e69418", - "sha256:b78d00e48261fbbd04aa0d7427cf78d18401ee0abd89c7559bbf422e5b1c7d01", - "sha256:b8b97a8a87cadcd3f94659b4ef6ec056261fa1e1c3317f4193ac231d4df70215", - "sha256:bd5b7ccae24e3d8501ee5563e82febc1771e73bd268eef82a1e8d2b4d556ae66", - "sha256:bdc02c0235b261925102b1bd586579b7158e9d0d07ecb61148a1799214a4afd5", - "sha256:be6b350dfbc7f708d9d853663772a9310783ea58f6035eec649fb9c4371b5389", - "sha256:c403c81bb8ffb1c993d0165a11493fd4bf1353d258f6997b3ee288b0a48fce77", - "sha256:cf8c6aed12a935abf2e290860af8e77b26a042eb7f2582ff83dc7ed5f963340c", - "sha256:d98addfd3c8728ee8b2c49126f3c44c703e2b005d4a95998e2167af176a9e722", - "sha256:dc76bca1ca98f4b122114435f83f1fcf3c0fe48e4e6f660e07996abf2f53903c", - "sha256:dec198619b7dbd6db58603cd256e092bcadef22a796f778bf87f8592b468441d", - "sha256:df28dda02c9328e122661f399f7655cdcbcf22ea42daa3650a26bce08a187450", - "sha256:e603ca1fb47b913942f3e660a15e55a9ebca906857edfea476ae5f0fe9b457d5", - "sha256:ecfdd68d334a6b97472ed032b5b37a30d8217c097acfff15e8452c710e775524" - ], - "index": "pypi", - "version": "==1.23.2" + "sha256:004f0efcb2fe1c0bd6ae1fcfc69cc8b6bf2407e0f18be308612007a0762b4089", + "sha256:09f6b7bdffe57fc61d869a22f506049825d707b288039d30f26a0d0d8ea05164", + "sha256:0ea3f98a0ffce3f8f57675eb9119f3f4edb81888b6874bc1953f91e0b1d4f440", + "sha256:17c0e467ade9bda685d5ac7f5fa729d8d3e76b23195471adae2d6a6941bd2c18", + "sha256:1f27b5322ac4067e67c8f9378b41c746d8feac8bdd0e0ffede5324667b8a075c", + "sha256:22d43376ee0acd547f3149b9ec12eec2f0ca4a6ab2f61753c5b29bb3e795ac4d", + "sha256:2ad3ec9a748a8943e6eb4358201f7e1c12ede35f510b1a2221b70af4bb64295c", + "sha256:301c00cf5e60e08e04d842fc47df641d4a181e651c7135c50dc2762ffe293dbd", + "sha256:39a664e3d26ea854211867d20ebcc8023257c1800ae89773cbba9f9e97bae036", + "sha256:51bf49c0cd1d52be0a240aa66f3458afc4b95d8993d2d04f0d91fa60c10af6cd", + "sha256:78a63d2df1d947bd9d1b11d35564c2f9e4b57898aae4626638056ec1a231c40c", + "sha256:7cd1328e5bdf0dee621912f5833648e2daca72e3839ec1d6695e91089625f0b4", + "sha256:8355fc10fd33a5a70981a5b8a0de51d10af3688d7a9e4a34fcc8fa0d7467bb7f", + "sha256:8c79d7cf86d049d0c5089231a5bcd31edb03555bd93d81a16870aa98c6cfb79d", + "sha256:91b8d6768a75247026e951dce3b2aac79dc7e78622fc148329135ba189813584", + "sha256:94c15ca4e52671a59219146ff584488907b1f9b3fc232622b47e2cf832e94fb8", + "sha256:98dcbc02e39b1658dc4b4508442a560fe3ca5ca0d989f0df062534e5ca3a5c1a", + "sha256:a64403f634e5ffdcd85e0b12c08f04b3080d3e840aef118721021f9b48fc1460", + "sha256:bc6e8da415f359b578b00bcfb1d08411c96e9a97f9e6c7adada554a0812a6cc6", + "sha256:bdc9febce3e68b697d931941b263c59e0c74e8f18861f4064c1f712562903411", + "sha256:c1ba66c48b19cc9c2975c0d354f24058888cdc674bebadceb3cdc9ec403fb5d1", + "sha256:c9f707b5bb73bf277d812ded9896f9512a43edff72712f31667d0a8c2f8e71ee", + "sha256:d5422d6a1ea9b15577a9432e26608c73a78faf0b9039437b075cf322c92e98e7", + "sha256:e5d5420053bbb3dd64c30e58f9363d7a9c27444c3648e61460c1237f9ec3fa14", + "sha256:e868b0389c5ccfc092031a861d4e158ea164d8b7fdbb10e3b5689b4fc6498df6", + "sha256:efd9d3abe5774404becdb0748178b48a218f1d8c44e0375475732211ea47c67e", + "sha256:f8c02ec3c4c4fcb718fdf89a6c6f709b14949408e8cf2a2be5bfa9c49548fd85", + "sha256:ffcf105ecdd9396e05a8e58e81faaaf34d3f9875f137c7372450baa5d77c9a54" + ], + "index": "pypi", + "version": "==1.23.3" }, "onnx": { "hashes": [ @@ -692,6 +696,7 @@ "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f", "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069", "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402", + "sha256:336b9036127eab855beec9662ac3ea13a4544a523ae273cbf108b228ecac8437", "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885", "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e", "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be", @@ -720,6 +725,7 @@ "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8", "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb", "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3", + "sha256:adabc0bce035467fb537ef3e5e74f2847c8af217ee0be0455d4fec8adc0462fc", "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf", "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1", "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a", @@ -793,41 +799,41 @@ }, "psutil": { "hashes": [ - "sha256:068935df39055bf27a29824b95c801c7a5130f118b806eee663cad28dca97685", - "sha256:0904727e0b0a038830b019551cf3204dd48ef5c6868adc776e06e93d615fc5fc", - "sha256:0f15a19a05f39a09327345bc279c1ba4a8cfb0172cc0d3c7f7d16c813b2e7d36", - "sha256:19f36c16012ba9cfc742604df189f2f28d2720e23ff7d1e81602dbe066be9fd1", - "sha256:20b27771b077dcaa0de1de3ad52d22538fe101f9946d6dc7869e6f694f079329", - "sha256:28976df6c64ddd6320d281128817f32c29b539a52bdae5e192537bc338a9ec81", - "sha256:29a442e25fab1f4d05e2655bb1b8ab6887981838d22effa2396d584b740194de", - "sha256:3054e923204b8e9c23a55b23b6df73a8089ae1d075cb0bf711d3e9da1724ded4", - "sha256:32c52611756096ae91f5d1499fe6c53b86f4a9ada147ee42db4991ba1520e574", - "sha256:3a76ad658641172d9c6e593de6fe248ddde825b5866464c3b2ee26c35da9d237", - "sha256:44d1826150d49ffd62035785a9e2c56afcea66e55b43b8b630d7706276e87f22", - "sha256:4b6750a73a9c4a4e689490ccb862d53c7b976a2a35c4e1846d049dcc3f17d83b", - "sha256:56960b9e8edcca1456f8c86a196f0c3d8e3e361320071c93378d41445ffd28b0", - "sha256:57f1819b5d9e95cdfb0c881a8a5b7d542ed0b7c522d575706a80bedc848c8954", - "sha256:58678bbadae12e0db55186dc58f2888839228ac9f41cc7848853539b70490021", - "sha256:645bd4f7bb5b8633803e0b6746ff1628724668681a434482546887d22c7a9537", - "sha256:799759d809c31aab5fe4579e50addf84565e71c1dc9f1c31258f159ff70d3f87", - "sha256:79c9108d9aa7fa6fba6e668b61b82facc067a6b81517cab34d07a84aa89f3df0", - "sha256:91c7ff2a40c373d0cc9121d54bc5f31c4fa09c346528e6a08d1845bce5771ffc", - "sha256:9272167b5f5fbfe16945be3db475b3ce8d792386907e673a209da686176552af", - "sha256:944c4b4b82dc4a1b805329c980f270f170fdc9945464223f2ec8e57563139cf4", - "sha256:a6a11e48cb93a5fa606306493f439b4aa7c56cb03fc9ace7f6bfa21aaf07c453", - "sha256:a8746bfe4e8f659528c5c7e9af5090c5a7d252f32b2e859c584ef7d8efb1e689", - "sha256:abd9246e4cdd5b554a2ddd97c157e292ac11ef3e7af25ac56b08b455c829dca8", - "sha256:b14ee12da9338f5e5b3a3ef7ca58b3cba30f5b66f7662159762932e6d0b8f680", - "sha256:b88f75005586131276634027f4219d06e0561292be8bd6bc7f2f00bdabd63c4e", - "sha256:c7be9d7f5b0d206f0bbc3794b8e16fb7dbc53ec9e40bbe8787c6f2d38efcf6c9", - "sha256:d2d006286fbcb60f0b391741f520862e9b69f4019b4d738a2a45728c7e952f1b", - "sha256:db417f0865f90bdc07fa30e1aadc69b6f4cad7f86324b02aa842034efe8d8c4d", - "sha256:e7e10454cb1ab62cc6ce776e1c135a64045a11ec4c6d254d3f7689c16eb3efd2", - "sha256:f65f9a46d984b8cd9b3750c2bdb419b2996895b005aefa6cbaba9a143b1ce2c5", - "sha256:fea896b54f3a4ae6f790ac1d017101252c93f6fe075d0e7571543510f11d2676" - ], - "index": "pypi", - "version": "==5.9.1" + "sha256:14b29f581b5edab1f133563272a6011925401804d52d603c5c606936b49c8b97", + "sha256:256098b4f6ffea6441eb54ab3eb64db9ecef18f6a80d7ba91549195d55420f84", + "sha256:39ec06dc6c934fb53df10c1672e299145ce609ff0611b569e75a88f313634969", + "sha256:404f4816c16a2fcc4eaa36d7eb49a66df2d083e829d3e39ee8759a411dbc9ecf", + "sha256:42638876b7f5ef43cef8dcf640d3401b27a51ee3fa137cb2aa2e72e188414c32", + "sha256:4642fd93785a29353d6917a23e2ac6177308ef5e8be5cc17008d885cb9f70f12", + "sha256:4fb54941aac044a61db9d8eb56fc5bee207db3bc58645d657249030e15ba3727", + "sha256:561dec454853846d1dd0247b44c2e66a0a0c490f937086930ec4b8f83bf44f06", + "sha256:5d39e3a2d5c40efa977c9a8dd4f679763c43c6c255b1340a56489955dbca767c", + "sha256:614337922702e9be37a39954d67fdb9e855981624d8011a9927b8f2d3c9625d9", + "sha256:67b33f27fc0427483b61563a16c90d9f3b547eeb7af0ef1b9fe024cdc9b3a6ea", + "sha256:68b35cbff92d1f7103d8f1db77c977e72f49fcefae3d3d2b91c76b0e7aef48b8", + "sha256:7cbb795dcd8ed8fd238bc9e9f64ab188f3f4096d2e811b5a82da53d164b84c3f", + "sha256:8f024fbb26c8daf5d70287bb3edfafa22283c255287cf523c5d81721e8e5d82c", + "sha256:91aa0dac0c64688667b4285fa29354acfb3e834e1fd98b535b9986c883c2ce1d", + "sha256:94e621c6a4ddb2573d4d30cba074f6d1aa0186645917df42c811c473dd22b339", + "sha256:9770c1d25aee91417eba7869139d629d6328a9422ce1cdd112bd56377ca98444", + "sha256:b1928b9bf478d31fdffdb57101d18f9b70ed4e9b0e41af751851813547b2a9ab", + "sha256:b2f248ffc346f4f4f0d747ee1947963613216b06688be0be2e393986fe20dbbb", + "sha256:b315febaebae813326296872fdb4be92ad3ce10d1d742a6b0c49fb619481ed0b", + "sha256:b3591616fa07b15050b2f87e1cdefd06a554382e72866fcc0ab2be9d116486c8", + "sha256:b4018d5f9b6651f9896c7a7c2c9f4652e4eea53f10751c4e7d08a9093ab587ec", + "sha256:d75291912b945a7351d45df682f9644540d564d62115d4a20d45fa17dc2d48f8", + "sha256:dc9bda7d5ced744622f157cc8d8bdd51735dafcecff807e928ff26bdb0ff097d", + "sha256:e3ac2c0375ef498e74b9b4ec56df3c88be43fe56cac465627572dbfb21c4be34", + "sha256:e4c4a7636ffc47b7141864f1c5e7d649f42c54e49da2dd3cceb1c5f5d29bfc85", + "sha256:ed29ea0b9a372c5188cdb2ad39f937900a10fb5478dc077283bf86eeac678ef1", + "sha256:f40ba362fefc11d6bea4403f070078d60053ed422255bd838cd86a40674364c9", + "sha256:f4cb67215c10d4657e320037109939b1c1d2fd70ca3d76301992f89fe2edb1f1", + "sha256:f7929a516125f62399d6e8e026129c8835f6c5a3aab88c3fff1a05ee8feb840d", + "sha256:fd331866628d18223a4265371fd255774affd86244fc307ef66eaf00de0633d5", + "sha256:feb861a10b6c3bb00701063b37e4afc754f8217f0f09c42280586bd6ac712b5c" + ], + "index": "pypi", + "version": "==5.9.2" }, "pycapnp": { "hashes": [ @@ -909,62 +915,62 @@ }, "pyjwt": { "hashes": [ - "sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf", - "sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba" + "sha256:8d82e7087868e94dd8d7d418e5088ce64f7daab4b36db654cbaedb46f9d1ca80", + "sha256:e77ab89480905d86998442ac5788f35333fa85f65047a534adc38edf3c88fc3b" ], "index": "pypi", - "version": "==2.4.0" + "version": "==2.5.0" }, "pylint": { "hashes": [ - "sha256:4b124affc198b7f7c9b5f9ab690d85db48282a025ef9333f51d2d7281b92a6c3", - "sha256:4f3f7e869646b0bd63b3dfb79f3c0f28fc3d2d923ea220d52620fd625aed92b0" + "sha256:5fdfd44af182866999e6123139d265334267339f29961f00c89783155eacc60b", + "sha256:7f6aad1d8d50807f7bc64f89ac75256a9baf8e6ed491cc9bc65592bc3f462cf1" ], "index": "pypi", - "version": "==2.15.0" + "version": "==2.15.3" }, "pyopencl": { "hashes": [ - "sha256:069e7eb1a223d88c13eafa54d6ae896fa892e75ba3d56ff2135a26107ef1142b", - "sha256:1490e6cdeaecba42854013c273685d65fd9102ee6dc6bc3bcb814e9e2b8179e5", - "sha256:15f7b3d29c9359e1e440e4f52f70de031f8d0d8d0f8de53a3bc01501b89360c0", - "sha256:1a2029b7fda6709eca077f618f997372c3d6f2780ad45512632b0d056e6305f9", - "sha256:25e87b4ccc0cc53487d445bea07ce9bdb478a335725df16986aead2ff65b68a4", - "sha256:2c9ad1cbc3f540afc52038851be8e06640aacfece051c89408bc3aece605a7ee", - "sha256:2df01c95ea9ae3dd66b277f0df47144cf7535a27b48a8d49fdd98e0583e368ce", - "sha256:316f59d0c40bfce4f6c160dbaf6501883b33880370bb1819f360dad747e52dfe", - "sha256:4836bc4619be967d6c28627adac151223037fdca056c4ab54da16b591f719347", - "sha256:4b53f7f3ed85ab671c8bfc61a0bbc5476725a7a5f51a94bba5512c3962b2d609", - "sha256:5304cb336af7316ae0650abb7467c076032635bfe4710b8df191612d245dca28", - "sha256:55e9302b8f0b1964c87b0fdab7b853aa2b2f10b4188f5b4618782d4380448c11", - "sha256:6032bef8a35f6df727a0b66e3c9faedb3f560318052848b28d2f72622cfbeace", - "sha256:6ec55934057e99461f684ccd293d87db59a452f5834c13ae36b19d31dfe38599", - "sha256:7176f96728be9b43024bd71704f60849cbfcf0fafd20270181b68ea4730ceb2d", - "sha256:730901d409d8251cd6e9dc59e6c518dff5cdb20a3a0b728344bfd2c707f28b64", - "sha256:75be43c7f33fb86f9d18b7b6f8e9081d8bd5b6331a90aec0d2cad3e81e72bc8f", - "sha256:7bef8e8bcfff574b481565390113ea0a37cf33fd2587ade7f2980f15e73f7b08", - "sha256:7ca9597877e1f8bdb4a49810988230f538b2d7aac389c33418a21cf4358f2fd4", - "sha256:814389b3eb9e6930cf43b984283c94a955edf20ec286402da5acfa503d3ae790", - "sha256:8efc3467454ce8c644f09029a3308496f9cb6e93ca5e8c08f6b79e7825da72c5", - "sha256:98bad7035f27b6de5c9268f52c1e10bffe3a2874994e862468a1792b699a4884", - "sha256:9bbfe94bb6e9d0458693183334e73c973e2fcba01568f42db15b453b926fb816", - "sha256:9d112a4426f5b356641c1312bf1004247dc4019e649502589b86333557203c01", - "sha256:a845779f505ed57b83f279307ae6307d886f3e41fb24dcf7889da27daa726118", - "sha256:aca3581f1a7f6b809b8cdc78b0e66587848b38b143bf2983e91ff8fb9a41bc8f", - "sha256:af5664b98140a29966c5fb12e9d29b85b6c6310efa97d82aee58310774917e8f", - "sha256:b85fa5ba1678dd40713587fd437787b6aa940000c2ddffa360884431be21723a", - "sha256:bcabfb5217ca8f8770f9c69298f79576080bb994b1883a99494b4c2668b04836", - "sha256:c00989bed1e7e5b32ad498fec3deb1c93403ab802cd99b7c78b9c692bd0910ef", - "sha256:d0ddc3b74ad1804eb3fe238dfa3b844b997e88b1ca5164a717c16b362b4f34c3", - "sha256:d8bb2eea4e960917e0a6132dedd34c8ec0b7a384f22713f775d50dbce154263a", - "sha256:db833ebb1e756969a8f851f15486598eb9e3fb27b0535c2a8193cc1c71455016", - "sha256:dc2d78cb5da0081ada1c263aaa773fd5479b3da5e2c421547bf7f3258d3239a5", - "sha256:dd2728e59ae088c900ed68f68d953476d0ff07189f182f917b74de2ac7b3972e", - "sha256:ea4eff6b922fa4ad2077ef90b3254d78597d050ada09bfbe74c22dd22d10c6ac", - "sha256:f8887d54e654598f3854472540b2eb228ac56b56a2491b95bdfac8f15be1c943" - ], - "index": "pypi", - "version": "==2022.1.6" + "sha256:024f7ad835f70fff2c27a3d111eec438761c42413ca78af20cc87ad3ecaba01a", + "sha256:03f3e96c8743edf16dceddef564c5198f8d988152a26a859de9d3e1e0f14888c", + "sha256:07e58b74a59cd27c390f3099597c1f05e50c441f82fb17fb84d43e5785951ecf", + "sha256:07e9ffe8d5d38066fd5a8a5540b6944617322b13d55db7e3f78609dc309e4da8", + "sha256:10bc9e39e6bb5c6f842dd4a3af869cb73a4ee29d47a74deed8120390c7bcf4ad", + "sha256:15d4843b88eb2379cb29d2ef13b93b6ce8f917bae70eb6c1584ba21c4c5c4ff6", + "sha256:21da5f08aabbc2b2fdc81466ffaac07c1db94b1f47a95bc57f633b55590ccba2", + "sha256:2f5010cfcd434c56e9bbad5fb4dba2fa64b1de6d8f9282b29a5d837268fe93cc", + "sha256:3361f7b1797cdd38c9fff54c6e34750a5665c9c573aab46fcb486fefdf6fcfca", + "sha256:4599fba8aff4c381d2f16651c025b4e6e9488100781a95634215755ea4935a0d", + "sha256:462f1ba6ee3769492bc376bdaeaf2f34f3528bd0fd0dc60569f3edf9346cc769", + "sha256:4890137300a18d94265f1342f5eb8de420029637a09020b682f09f63271170c5", + "sha256:4e55c1df1976040c01315dc09477c7369422e67f36dcf69d9f570bdf48759802", + "sha256:58641144140025b39dbf27a095e250018426a25973e7cfd87724b24c6c8a6a3e", + "sha256:5b8f4358d6bdcf719c46f91c0e9a8ff8ece05843f26de1d047ea1b1ae1875aaf", + "sha256:647ff48cb851040650d79d99eae0229bfd9cd8d931ffae8d33a887891be2b8f4", + "sha256:71ec7b12833a9e29807dc95421791c5ebed8f9435f3e99c5ac77f9f409429863", + "sha256:80db5d46608dfa9e37109bfb925be64cb8d24b06e27945cf7c8996fa9e64ae50", + "sha256:8189b95ac0c845b2639395d5dffe2c4406ac5e42c61b8db972e5dc596e5b8685", + "sha256:8906173bf2d3bc036e0acc8e4d785fc33c89951bacbb0049d64e75d7c476beed", + "sha256:9fbd13defa6f7d719540433890b5318f16a8c3c453dd5feb66a6054a79144c01", + "sha256:a238dd164b141dd9eea0fbab39f0d247bd3c5f8d31b85997aa5619c43a6cb9f5", + "sha256:a2a00fbe65fcebf071a6af59e96fc6b03c8dc95c0d5623cd7d96a083930ffb31", + "sha256:a41e0b02b4c9e6bfdf880d8f6d8d5a21fdd04cd7e7f2afd4d0c5c94994963530", + "sha256:a6bb5e7cb9612b011da83a274ee5477ee9c8892860bae5f3f105c80b512192f9", + "sha256:a78827b7d0f5ee22890a950d08cd2516b136c6af10abad696e5a8f4b62d55db5", + "sha256:b0711e2508fbf9de1d26817f79f57a43b6d84f088f6e40208582e7173a0a3b22", + "sha256:bdbbe72731fdc8152b555360efec791f9c5b71f0eed033052302d51547ca00cb", + "sha256:c4d734261075c750fe7347eb07cdc155d53deb4175be2b9d73524bcc9868072c", + "sha256:c6bd1e1d086d4b28322e88d22deeff06684360f3a9017d7d7eb2f1ae48095479", + "sha256:c8835ad22ac0e656d5b437abb99c763bb4529485c858216015c1286d6a919159", + "sha256:cd322df0f8bec18445e3be0d9dfb7899ed570e520949d337dfb42cc3306e835a", + "sha256:d04291fa318c7ce1033fba5e16f5ca5f7b28dab49213596b5139bac51812cd69", + "sha256:d1eecc9bd2c2dbc32b6f550b7e9264bb8e8bef8520e75fd4dbc627c21faebaf1", + "sha256:d493b83e1be953e7d7849704a0970d2e6915fb5b3514222ab8a6abd3d47e123e", + "sha256:dae8527318f60b557103d4d385892a86c3f470eeed1f560b830d1e8a5724a4b5", + "sha256:e0e81c075fb1c21a3080259fe7de771f1b06861140901c22e47f4e8a69bd8984" + ], + "index": "pypi", + "version": "==2022.2.3" }, "pyparsing": { "hashes": [ @@ -1011,6 +1017,7 @@ }, "pyyaml": { "hashes": [ + "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", @@ -1022,26 +1029,32 @@ "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" ], @@ -1050,82 +1063,83 @@ }, "pyzmq": { "hashes": [ - "sha256:022cf5ea7bcaa8a06a03c2706e0ae66904b6138b2155577cd34c64bc7cc637ab", - "sha256:044447ae4b2016a6b8697571fd633f799f860b19b76c4a2fd9b1140d52ee6745", - "sha256:07ed8aaf7ffe150af873269690cc654ffeca7491f62aae0f3821baa181f8d5fe", - "sha256:10d1910ec381b851aeb024a042a13db178cb1edf125e76a4e9d2548ad103aadb", - "sha256:12e62ff0d5223ec09b597ab6d73858b9f64a51221399f3cb08aa495e1dff7935", - "sha256:1f368a82b29f80071781b20663c0fc0c8f6b13273f9f5abe1526af939534f90f", - "sha256:20bafc4095eab00f41a510579363a3f5e1f5c69d7ee10f1d88895c4df0259183", - "sha256:2141e6798d5981be04c08996d27962086a1aa3ea536fe9cf7e89817fd4523f86", - "sha256:23e708fbfdf4ee3107422b69ca65da1b9f056b431fc0888096a8c1d6cd908e8f", - "sha256:28dbdb90b2f6b131f8f10e6081012e4e25234213433420e67e0c1162de537113", - "sha256:29b74774a0bfd3c4d98ac853f0bdca55bd9ec89d5b0def5486407cca54472ef8", - "sha256:2b381aa867ece7d0a82f30a0c7f3d4387b7cf2e0697e33efaa5bed6c5784abcd", - "sha256:2f67b63f53c6994d601404fd1a329e6d940ac3dd1d92946a93b2b9c70df67b9f", - "sha256:342ca3077f47ec2ee41b9825142b614e03e026347167cbc72a59b618c4f6106c", - "sha256:35e635343ff367f697d00fa1484262bb68e36bc74c9b80737eac5a1e04c4e1b1", - "sha256:385609812eafd9970c3752c51f2f6c4f224807e3e441bcfd8c8273877d00c8a8", - "sha256:38e106b64bad744fe469dc3dd864f2764d66399178c1bf39d45294cc7980f14f", - "sha256:39dd252b683816935702825e5bf775df16090619ced9bb4ba68c2d0b6f0c9b18", - "sha256:407f909c4e8fde62fbdad9ebd448319792258cc0550c2815567a4d9d8d9e6d18", - "sha256:415ff62ac525d9add1e3550430a09b9928d2d24a20cc4ce809e67caac41219ab", - "sha256:4805af9614b0b41b7e57d17673459facf85604dac502a5a9244f6e8c9a4de658", - "sha256:48400b96788cdaca647021bf19a9cd668384f46e4d9c55cf045bdd17f65299c8", - "sha256:49d30ba7074f469e8167917abf9eb854c6503ae10153034a6d4df33618f1db5f", - "sha256:4bb798bef181648827019001f6be43e1c48b34b477763b37a8d27d8c06d197b8", - "sha256:4d6f110c56f7d5b4d64dde3a382ae61b6d48174e30742859d8e971b18b6c9e5c", - "sha256:55568a020ad2cae9ae36da6058e7ca332a56df968f601cbdb7cf6efb2a77579a", - "sha256:565bd5ab81f6964fc4067ccf2e00877ad0fa917308975694bbb54378389215f8", - "sha256:5c558b50402fca1acc94329c5d8f12aa429738904a5cfb32b9ed3c61235221bb", - "sha256:5e05492be125dce279721d6b54fd1b956546ecc4bcdfcf8e7b4c413bc0874c10", - "sha256:624fd38071a817644acdae075b92a23ea0bdd126a58148288e8284d23ec361ce", - "sha256:650389bbfca73955b262b2230423d89992f38ec48033307ae80e700eaa2fbb63", - "sha256:67975a9e1237b9ccc78f457bef17691bbdd2055a9d26e81ee914ba376846d0ce", - "sha256:6b1e79bba24f6df1712e3188d5c32c480d8eda03e8ecff44dc8ecb0805fa62f3", - "sha256:6fd5d0d50cbcf4bc376861529a907bed026a4cbe8c22a500ff8243231ef02433", - "sha256:71b32a1e827bdcbf73750e60370d3b07685816ff3d8695f450f0f8c3226503f8", - "sha256:794871988c34727c7f79bdfe2546e6854ae1fa2e1feb382784f23a9c6c63ecb3", - "sha256:79a87831b47a9f6161ad23fa5e89d5469dc585abc49f90b9b07fea8905ae1234", - "sha256:7e0113d70b095339e99bb522fe7294f5ae6a7f3b2b8f52f659469a74b5cc7661", - "sha256:84678153432241bcdca2210cf4ff83560b200556867aea913ffbb960f5d5f340", - "sha256:8a68f57b7a3f7b6b52ada79876be1efb97c8c0952423436e84d70cc139f16f0d", - "sha256:8c02a0cd39dc01659b3d6cb70bb3a41aebd9885fd78239acdd8d9c91351c4568", - "sha256:8c842109d31a9281d678f668629241c405928afbebd913c48a5a8e7aee61f63d", - "sha256:8dc66f109a245653b19df0f44a5af7a3f14cb8ad6c780ead506158a057bd36ce", - "sha256:90d88f9d9a2ae6cfb1dc4ea2d1710cdf6456bc1b9a06dd1bb485c5d298f2517e", - "sha256:9269fbfe3a4eb2009199120861c4571ef1655fdf6951c3e7f233567c94e8c602", - "sha256:929d548b74c0f82f7f95b54e4a43f9e4ce2523cfb8a54d3f7141e45652304b2a", - "sha256:99a5a77a10863493a1ee8dece02578c6b32025fb3afff91b40476bc489e81648", - "sha256:9a39ddb0431a68954bd318b923230fa5b649c9c62b0e8340388820c5f1b15bd2", - "sha256:9d0ab2936085c85a1fc6f9fd8f89d5235ae99b051e90ec5baa5e73ad44346e1f", - "sha256:9e5bf6e7239fc9687239de7a283aa8b801ab85371116045b33ae20132a1325d6", - "sha256:a0f09d85c45f58aa8e715b42f8b26beba68b3b63a8f7049113478aca26efbc30", - "sha256:a114992a193577cb62233abf8cb2832970f9975805a64740e325d2f895e7f85a", - "sha256:a3fd44b5046d247e7f0f1660bcafe7b5fb0db55d0934c05dd57dda9e1f823ce7", - "sha256:ad28ddb40db8e450d7d4bf8a1d765d3f87b63b10e7e9a825a3c130c6371a8c03", - "sha256:aecd6ceaccc4b594e0092d6513ef3f1c0fa678dd89f86bb8ff1a47014b8fca35", - "sha256:b815991c7d024bf461f358ad871f2be1135576274caed5749c4828859e40354e", - "sha256:b861db65f6b8906c8d6db51dde2448f266f0c66bf28db2c37aea50f58a849859", - "sha256:c3ebf1668664d20c8f7d468955f18379b7d1f7bc8946b13243d050fa3888c7ff", - "sha256:c56b1a62a1fb87565343c57b6743fd5da6e138b8c6562361d7d9b5ce4acf399a", - "sha256:c780acddd2934c6831ff832ecbf78a45a7b62d4eb216480f863854a8b7d54fa7", - "sha256:c890309296f53f9aa32ffcfc51d805705e1982bffd27c9692a8f1e1b8de279f4", - "sha256:c9cfaf530e6a7ff65f0afe275e99f983f68b54dfb23ea401f0bc297a632766b6", - "sha256:d904f6595acfaaf99a1a61881fea068500c40374d263e5e073aa4005e5f9c28a", - "sha256:e06747014a5ad1b28cebf5bc1ddcdaccfb44e9b441d35e6feb1286c8a72e54be", - "sha256:e1fe30bcd5aea5948c42685fad910cd285eacb2518ea4dc6c170d6b535bee95d", - "sha256:e753eee6d3b93c5354e8ba0a1d62956ee49355f0a36e00570823ef64e66183f5", - "sha256:ec9803aca9491fd6f0d853d2a6147f19f8deaaa23b1b713d05c5d09e56ea7142", - "sha256:efb9e38b2a590282704269585de7eb33bf43dc294cad092e1b172e23d4c217e5", - "sha256:f07016e3cf088dbfc6e7c5a7b3f540db5c23b0190d539e4fd3e2b5e6beffa4b5", - "sha256:f392cbea531b7142d1958c0d4a0c9c8d760dc451e5848d8dd3387804d3e3e62c", - "sha256:f619fd38fc2641abfb53cca719c165182500600b82c695cc548a0f05f764be05", - "sha256:fefdf9b685fda4141b95ebec975946076a5e0723ff70b037032b2085c5317684", - "sha256:ffc6b1623d0f9affb351db4ca61f432dca3628a5ee015f9bf2bfbe9c6836881c" - ], - "index": "pypi", - "version": "==23.2.1" + "sha256:0108358dab8c6b27ff6b985c2af4b12665c1bc659648284153ee501000f5c107", + "sha256:07bec1a1b22dacf718f2c0e71b49600bb6a31a88f06527dfd0b5aababe3fa3f7", + "sha256:0e8f482c44ccb5884bf3f638f29bea0f8dc68c97e38b2061769c4cb697f6140d", + "sha256:0ec91f1bad66f3ee8c6deb65fa1fe418e8ad803efedd69c35f3b5502f43bd1dc", + "sha256:0f14cffd32e9c4c73da66db97853a6aeceaac34acdc0fae9e5bbc9370281864c", + "sha256:15975747462ec49fdc863af906bab87c43b2491403ab37a6d88410635786b0f4", + "sha256:1724117bae69e091309ffb8255412c4651d3f6355560d9af312d547f6c5bc8b8", + "sha256:1a7c280185c4da99e0cc06c63bdf91f5b0b71deb70d8717f0ab870a43e376db8", + "sha256:1b7928bb7580736ffac5baf814097be342ba08d3cfdfb48e52773ec959572287", + "sha256:2032d9cb994ce3b4cba2b8dfae08c7e25bc14ba484c770d4d3be33c27de8c45b", + "sha256:20e7eeb1166087db636c06cae04a1ef59298627f56fb17da10528ab52a14c87f", + "sha256:216f5d7dbb67166759e59b0479bca82b8acf9bed6015b526b8eb10143fb08e77", + "sha256:28b119ba97129d3001673a697b7cce47fe6de1f7255d104c2f01108a5179a066", + "sha256:3104f4b084ad5d9c0cb87445cc8cfd96bba710bef4a66c2674910127044df209", + "sha256:3e6192dbcefaaa52ed81be88525a54a445f4b4fe2fffcae7fe40ebb58bd06bfd", + "sha256:42d4f97b9795a7aafa152a36fe2ad44549b83a743fd3e77011136def512e6c2a", + "sha256:44e706bac34e9f50779cb8c39f10b53a4d15aebb97235643d3112ac20bd577b4", + "sha256:47b11a729d61a47df56346283a4a800fa379ae6a85870d5a2e1e4956c828eedc", + "sha256:4854f9edc5208f63f0841c0c667260ae8d6846cfa233c479e29fdc85d42ebd58", + "sha256:48f721f070726cd2a6e44f3c33f8ee4b24188e4b816e6dd8ba542c8c3bb5b246", + "sha256:52afb0ac962963fff30cf1be775bc51ae083ef4c1e354266ab20e5382057dd62", + "sha256:54d8b9c5e288362ec8595c1d98666d36f2070fd0c2f76e2b3c60fbad9bd76227", + "sha256:5bd3d7dfd9cd058eb68d9a905dec854f86649f64d4ddf21f3ec289341386c44b", + "sha256:613010b5d17906c4367609e6f52e9a2595e35d5cc27d36ff3f1b6fa6e954d944", + "sha256:624321120f7e60336be8ec74a172ae7fba5c3ed5bf787cc85f7e9986c9e0ebc2", + "sha256:65c94410b5a8355cfcf12fd600a313efee46ce96a09e911ea92cf2acf6708804", + "sha256:6640f83df0ae4ae1104d4c62b77e9ef39be85ebe53f636388707d532bee2b7b8", + "sha256:687700f8371643916a1d2c61f3fdaa630407dd205c38afff936545d7b7466066", + "sha256:77c2713faf25a953c69cf0f723d1b7dd83827b0834e6c41e3fb3bbc6765914a1", + "sha256:78068e8678ca023594e4a0ab558905c1033b2d3e806a0ad9e3094e231e115a33", + "sha256:7a23ccc1083c260fa9685c93e3b170baba45aeed4b524deb3f426b0c40c11639", + "sha256:7abddb2bd5489d30ffeb4b93a428130886c171b4d355ccd226e83254fcb6b9ef", + "sha256:80093b595921eed1a2cead546a683b9e2ae7f4a4592bb2ab22f70d30174f003a", + "sha256:8242543c522d84d033fe79be04cb559b80d7eb98ad81b137ff7e0a9020f00ace", + "sha256:838812c65ed5f7c2bd11f7b098d2e5d01685a3f6d1f82849423b570bae698c00", + "sha256:83ea1a398f192957cb986d9206ce229efe0ee75e3c6635baff53ddf39bd718d5", + "sha256:8421aa8c9b45ea608c205db9e1c0c855c7e54d0e9c2c2f337ce024f6843cab3b", + "sha256:858375573c9225cc8e5b49bfac846a77b696b8d5e815711b8d4ba3141e6e8879", + "sha256:86de64468cad9c6d269f32a6390e210ca5ada568c7a55de8e681ca3b897bb340", + "sha256:87f7ac99b15270db8d53f28c3c7b968612993a90a5cf359da354efe96f5372b4", + "sha256:8bad8210ad4df68c44ff3685cca3cda448ee46e20d13edcff8909eba6ec01ca4", + "sha256:8bb4af15f305056e95ca1bd086239b9ebc6ad55e9f49076d27d80027f72752f6", + "sha256:8c78bfe20d4c890cb5580a3b9290f700c570e167d4cdcc55feec07030297a5e3", + "sha256:8f3f3154fde2b1ff3aa7b4f9326347ebc89c8ef425ca1db8f665175e6d3bd42f", + "sha256:94010bd61bc168c103a5b3b0f56ed3b616688192db7cd5b1d626e49f28ff51b3", + "sha256:941fab0073f0a54dc33d1a0460cb04e0d85893cb0c5e1476c785000f8b359409", + "sha256:9dca7c3956b03b7663fac4d150f5e6d4f6f38b2462c1e9afd83bcf7019f17913", + "sha256:a180dbd5ea5d47c2d3b716d5c19cc3fb162d1c8db93b21a1295d69585bfddac1", + "sha256:a2712aee7b3834ace51738c15d9ee152cc5a98dc7d57dd93300461b792ab7b43", + "sha256:a435ef8a3bd95c8a2d316d6e0ff70d0db524f6037411652803e118871d703333", + "sha256:abb756147314430bee5d10919b8493c0ccb109ddb7f5dfd2fcd7441266a25b75", + "sha256:abe6eb10122f0d746a0d510c2039ae8edb27bc9af29f6d1b05a66cc2401353ff", + "sha256:acbd0a6d61cc954b9f535daaa9ec26b0a60a0d4353c5f7c1438ebc88a359a47e", + "sha256:ae08ac90aa8fa14caafc7a6251bd218bf6dac518b7bff09caaa5e781119ba3f2", + "sha256:ae61446166983c663cee42c852ed63899e43e484abf080089f771df4b9d272ef", + "sha256:afe1f3bc486d0ce40abb0a0c9adb39aed3bbac36ebdc596487b0cceba55c21c1", + "sha256:b946da90dc2799bcafa682692c1d2139b2a96ec3c24fa9fc6f5b0da782675330", + "sha256:b947e264f0e77d30dcbccbb00f49f900b204b922eb0c3a9f0afd61aaa1cedc3d", + "sha256:bb5635c851eef3a7a54becde6da99485eecf7d068bd885ac8e6d173c4ecd68b0", + "sha256:bcbebd369493d68162cddb74a9c1fcebd139dfbb7ddb23d8f8e43e6c87bac3a6", + "sha256:c31805d2c8ade9b11feca4674eee2b9cce1fec3e8ddb7bbdd961a09dc76a80ea", + "sha256:c8840f064b1fb377cffd3efeaad2b190c14d4c8da02316dae07571252d20b31f", + "sha256:ccb94342d13e3bf3ffa6e62f95b5e3f0bc6bfa94558cb37f4b3d09d6feb536ff", + "sha256:d66689e840e75221b0b290b0befa86f059fb35e1ee6443bce51516d4d61b6b99", + "sha256:dabf1a05318d95b1537fd61d9330ef4313ea1216eea128a17615038859da3b3b", + "sha256:db03704b3506455d86ec72c3358a779e9b1d07b61220dfb43702b7b668edcd0d", + "sha256:de4217b9eb8b541cf2b7fde4401ce9d9a411cc0af85d410f9d6f4333f43640be", + "sha256:df0841f94928f8af9c7a1f0aaaffba1fb74607af023a152f59379c01c53aee58", + "sha256:dfb992dbcd88d8254471760879d48fb20836d91baa90f181c957122f9592b3dc", + "sha256:e7e66b4e403c2836ac74f26c4b65d8ac0ca1eef41dfcac2d013b7482befaad83", + "sha256:e8012bce6836d3f20a6c9599f81dfa945f433dab4dbd0c4917a6fb1f998ab33d", + "sha256:f01de4ec083daebf210531e2cca3bdb1608dbbbe00a9723e261d92087a1f6ebc", + "sha256:f0d945a85b70da97ae86113faf9f1b9294efe66bd4a5d6f82f2676d567338b66", + "sha256:fa0ae3275ef706c0309556061185dd0e4c4cd3b7d6f67ae617e4e677c7a41e2e" + ], + "index": "pypi", + "version": "==24.0.1" }, "requests": { "hashes": [ @@ -1145,11 +1159,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:2d7ec7bc88ebbdf2c4b6b2650b3257893d386325a96c9b723adcd31033469b63", - "sha256:b4b41f90951ed83e7b4c176eef021b19ecba39da5b73aca106c97a9b7e279a90" + "sha256:d6c71d2f85710b66822adaa954af7912bab135d6c85febd5b0f3dfd4ab37e181", + "sha256:ef925b5338625448645a778428d8f22a3d17de8b28cc8e6fba60b93393ad86fe" ], "index": "pypi", - "version": "==1.9.5" + "version": "==1.9.9" }, "setproctitle": { "hashes": [ @@ -1219,11 +1233,11 @@ }, "setuptools": { "hashes": [ - "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82", - "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57" + "sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9", + "sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1" ], "markers": "python_version >= '3.7'", - "version": "==65.3.0" + "version": "==65.4.0" }, "shiboken2": { "hashes": [ @@ -1255,19 +1269,19 @@ }, "sympy": { "hashes": [ - "sha256:1fe96b4c56bb7a7630cdf150a6cd98bc97a79e6be233e30502aba1cf54dee33d", - "sha256:b53069f5f30e4a4690b57cdb8e3d0d9065fff42627239db718214f804e442481" + "sha256:938f984ee2b1e8eae8a07b884c8b7a1146010040fccddc6539c54f401c8f6fcf", + "sha256:e32380dce63cb7c0108ed525570092fd45168bdae2faa17e528221ef72e88658" ], "index": "pypi", - "version": "==1.11" + "version": "==1.11.1" }, "timezonefinder": { "hashes": [ - "sha256:2791ad459b85c110226057cb5ebdd6503f4fb0a33cc4f76fb93e98ed545edd68", - "sha256:406bea77a7baec5e2b1c2b4793ff8f40c174b6d8e894631e60864d956139afef" + "sha256:96c96db94e75e072187843152e6c5dc0718500a9a91986032365abe09162d0e7", + "sha256:f2ee561b1e7692b933fcd914df38800e93db7caf278e7328de7328829b04f275" ], "index": "pypi", - "version": "==6.1.1" + "version": "==6.1.3" }, "tomli": { "hashes": [ @@ -1282,16 +1296,16 @@ "sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c", "sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", + "markers": "python_version >= '3.6' and python_version < '4'", "version": "==0.11.4" }, "tqdm": { "hashes": [ - "sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d", - "sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6" + "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4", + "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1" ], "index": "pypi", - "version": "==4.64.0" + "version": "==4.64.1" }, "typing-extensions": { "hashes": [ @@ -1318,11 +1332,11 @@ }, "websocket-client": { "hashes": [ - "sha256:33ad3cf0aef4270b95d10a5a66b670a66be1f5ccf10ce390b3644f9eddfdca9d", - "sha256:79d730c9776f4f112f33b10b78c8d209f23b5806d9a783e296b3813fc5add2f1" + "sha256:398909eb7e261f44b8f4bd474785b6ec5f5b499d4953342fe9755e01ef624090", + "sha256:f9611eb65c8241a67fb373bef040b3cf8ad377a9f6546a12b620b6511e8ea9ef" ], "index": "pypi", - "version": "==1.4.0" + "version": "==1.4.1" }, "werkzeug": { "hashes": [ @@ -1543,11 +1557,11 @@ }, "certifi": { "hashes": [ - "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", - "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" + "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", + "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" ], "markers": "python_version >= '3.6'", - "version": "==2022.6.15" + "version": "==2022.9.24" }, "cffi": { "hashes": [ @@ -1635,6 +1649,81 @@ "markers": "python_version >= '3.6'", "version": "==2.1.1" }, + "contourpy": { + "hashes": [ + "sha256:0389349875424aa8c5e61f757e894687916bc4e9616cc6afcbd8051aa2428952", + "sha256:0395ae71164bfeb2dedd136e03c71a2718a5aa9873a46f518f4133be0d63e1d2", + "sha256:057114f698ffb9e54657e8fda6802e2f5c8fad609845cf6afaf31590ef6a33c0", + "sha256:061e1f066c419ffe25b615a1df031b4832ea1d7f2676937e69e8e00e24512005", + "sha256:06c4d1dde5ee4f909a8a95ba1eb04040c6c26946b4f3b5beaf10d45f14e940ee", + "sha256:09ed9b63f4df8a7591b7a4a26c1ad066dcaafda1f846250fdcb534074a411692", + "sha256:0f7672148f8fca48e4efc16aba24a7455b40c22d4f8abe42475dec6a12b0bb9a", + "sha256:0f89f0608a5aa8142ed0e53957916623791a88c7f5e5f07ae530c328beeb888f", + "sha256:128bd7acf569f8443ad5b2227f30ac909e4f5399ed221727eeacf0c6476187e6", + "sha256:19ea64fa0cf389d2ebc10974616acfa1fdecbd73d1fd9c72215b782f3c40f561", + "sha256:1fb782982c42cee667b892a0b0c52a9f6c7ecf1da5c5f4345845f04eaa862f93", + "sha256:218722a29c5c26677d37c44f5f8a372daf6f07870aad793a97d47eb6ad6b3290", + "sha256:2b5e334330d82866923015b455260173cb3b9e3b4e297052d758abd262031289", + "sha256:2bf5c846c257578b03d498b20f54f53551616a507d8e5463511c58bb58e9a9cf", + "sha256:2d0ad9a85f208473b1f3613c45756c7aa6fcc288266a8c7b873f896aaf741b6b", + "sha256:2f54dcc9bb9390fd0636301ead134d46d5229fe86da0db4d974c0fda349f560e", + "sha256:3109fa601d2a448cec4643abd3a31f972bf05b7c2f2e83df9d3429878f8c10ae", + "sha256:3210d93ad2af742b6a96cf39792f7181822edbb8fe11c3ef29d1583fe637a8d8", + "sha256:3b3082ade8849130203d461b98c2a061b382c46074b43b4edd5cefd81af92b8a", + "sha256:3c3f2f6b898a40207843ae01970e57e33d22a26b22f23c6a5e07b4716751085f", + "sha256:3ca40d7844b391d90b864c6a6d1bb6b88b09035fb4d866d64d43c4d26fb0ab64", + "sha256:3cfc067ddde78b76dcbc9684d82688b7d3c5158fa2254a085f9bcb9586c1e2d8", + "sha256:434942fa2f9019b9ae525fb752dc523800c49a1a28fbd6d9240b0fa959573dcc", + "sha256:46b8e24813e2fb5a3e598c1f8b9ae403e1438cb846a80cc2b33cddf19dddd7f2", + "sha256:59c827e536bb5e3ef58e06da0faba61fd89a14f30b68bcfeca41f43ca83a1942", + "sha256:60f37acd4e4227c5a29f737d9a85ca3145c529a8dd4bf70af7f0637c61b49222", + "sha256:689d7d2a840619915d0abd1ecc6e399fee202f8ad315acda2807f4ca420d0802", + "sha256:6c02e22cf09996194bcb3a4784099975cf527d5c29caf759abadf29ebdb2fe27", + "sha256:79908b9d02b1d6c1c71ff3b7ad127f3f82e14a8e091ab44b3c7e34b649fea733", + "sha256:7c9e99aac7b430f6a9f15eebf058c742097cea3369f23a2bfc5e64d374b67e3a", + "sha256:813c2944e940ef8dccea71305bacc942d4b193a021140874b3e58933ec44f5b6", + "sha256:87121b9428ac568fb84fae4af5e7852fc34f02eadc4e3e91f6c8989327692186", + "sha256:896631cd40222aef3697e4e51177d14c3709fda49d30983269d584f034acc8a4", + "sha256:970a4be7ec84ccda7c27cb4ae74930bbbd477bc8d849ed55ea798084dd5fca8c", + "sha256:9939796abcadb2810a63dfb26ff8ca4595fe7dd70a3ceae7f607a2639b714307", + "sha256:99a8071e351b50827ad976b92ed91845fb614ac67a3c41109b24f3d8bd3afada", + "sha256:9c16fa267740d67883899e054cccb4279e002f3f4872873b752c1ba15045ff49", + "sha256:a30e95274f5c0e007ccc759ec258aa5708c534ec058f153ee25ac700a2f1438b", + "sha256:a74afd8d560eaafe0d9e3e1db8c06081282a05ca4de00ee416195085a79d7d3d", + "sha256:b46a04588ceb7cf132568e0e564a854627ef87a1ed3bf536234540a79ced44b0", + "sha256:b4963cf08f4320d98ae72ec7694291b8ab85cb7da3b0cd824bc32701bc992edf", + "sha256:b50e481a4317a8efcfffcfddcd4c9b36eacba440440e70cbe0256aeb6fd6abae", + "sha256:b85553699862c09937a7a5ea14ee6229087971a7d51ae97d5f4b407f571a2c17", + "sha256:bcc98d397c3dea45d5b262029564b29cb8e945f2607a38bee6163694c0a8b4ef", + "sha256:bed3a2a823a041e8d249b1a7ec132933e1505299329b5cfe1b2b5ec689ec7675", + "sha256:bf6b4c0c723664f65c2a47c8cb6ebbf660b0b2e2d936adf2e8503d4e93359465", + "sha256:bfd634cb9685161b2a51f73a7fc4736fd0d67a56632d52319317afaa27f08243", + "sha256:c0d5ee865b5fd16bf62d72122aadcc90aab296c30c1adb0a32b4b66bd843163e", + "sha256:c2b4eab7c12f9cb460509bc34a3b086f9802f0dba27c89a63df4123819ad64af", + "sha256:c51568e94f7f232296de30002f2a50f77a7bd346673da3e4f2aaf9d2b833f2e5", + "sha256:c5158616ab39d34b76c50f40c81552ee180598f7825dc7a66fd187d29958820f", + "sha256:cdacddb18d55ffec42d1907079cdc04ec4fa8a990cdf5b9d9fe67d281fc0d12e", + "sha256:ce763369e646e59e4ca2c09735cd1bdd3048d909ad5f2bc116e83166a9352f3c", + "sha256:d45822b0a2a452327ab4f95efe368d234d5294bbf89a99968be27c7938a21108", + "sha256:d8150579bf30cdf896906baf256aa200cd50dbe6e565c17d6fd3d678e21ff5de", + "sha256:d88814befbd1433152c5f6dd536905149ba028d795a22555b149ae0a36024d9e", + "sha256:dca5be83a6dfaf933a46e3bc2b9f2685e5ec61b22f6a38ad740aac9c16e9a0ff", + "sha256:dd084459ecdb224e617e4ab3f1d5ebe4d1c48facb41f24952b76aa6ba9712bb0", + "sha256:def9a01b73c9e27d70ea03b381fb3e7aadfac1f398dbd63751313c3a46747ef5", + "sha256:df65f4b2b4e74977f0336bef12a88051ab24e6a16873cd9249f34d67cb3e345d", + "sha256:dfe924e5a63861c82332a12adeeab955dc8c8009ddbbd80cc2fcca049ff89a49", + "sha256:e67dcaa34dcd908fcccbf49194211d847c731b6ebaac661c1c889f1bf6af1e44", + "sha256:eba62b7c21a33e72dd8adab2b92dd5610d8527f0b2ac28a8e0770e71b21a13f9", + "sha256:ed9c91bf4ce614efed5388c3f989a7cfe08728ab871d995a486ea74ff88993db", + "sha256:f05d311c937da03b0cd26ac3e14cb991f6ff8fc94f98b3df9713537817539795", + "sha256:f1cc623fd6855b25da52b3275e0c9e51711b86a9dccc75f8c9ab4432fd8e42c7", + "sha256:f670686d99c867d0f24b28ce8c6f02429c6eef5e2674aab287850d0ee2d20437", + "sha256:f856652f9b533c6cd2b9ad6836a7fc0e43917d7ff15be46c5baf1350f8cdc5d9", + "sha256:fb0458d74726937ead9e2effc91144aea5a58ecee9754242f8539a782bed685a" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.5" + }, "control": { "hashes": [ "sha256:0891d2d32d6006ac1faa4e238ed8223ca342a4721d202dfeccae24fb02563183" @@ -1700,31 +1789,35 @@ }, "cryptography": { "hashes": [ - "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59", - "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596", - "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3", - "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5", - "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab", - "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884", - "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82", - "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b", - "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441", - "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa", - "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d", - "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b", - "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a", - "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6", - "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157", - "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280", - "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282", - "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67", - "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8", - "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046", - "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327", - "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9" - ], - "index": "pypi", - "version": "==37.0.4" + "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a", + "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f", + "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0", + "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407", + "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7", + "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6", + "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153", + "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750", + "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad", + "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6", + "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b", + "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5", + "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a", + "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d", + "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d", + "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294", + "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0", + "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a", + "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac", + "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61", + "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013", + "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e", + "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb", + "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9", + "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd", + "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818" + ], + "index": "pypi", + "version": "==38.0.1" }, "cycler": { "hashes": [ @@ -1818,11 +1911,11 @@ }, "fonttools": { "hashes": [ - "sha256:4606e1a88ee1f6699d182fea9511bd9a8a915d913eab4584e5226da1180fcce7", - "sha256:fff6b752e326c15756c819fe2fe7ceab69f96a1dbcfe8911d0941cdb49905007" + "sha256:a5bc5f5d48faa4085310b8ebd4c5d33bf27c6636c5f10a7de792510af2745a81", + "sha256:f32ef6ec966cf0e7d2aa88601fed2e3a8f2851c26b5db2c80ccc8f82bee4eedc" ], "markers": "python_version >= '3.7'", - "version": "==4.37.1" + "version": "==4.37.3" }, "ft4222": { "hashes": [ @@ -1876,19 +1969,19 @@ }, "identify": { "hashes": [ - "sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893", - "sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44" + "sha256:322a5699daecf7c6fd60e68852f36f2ecbb6a36ff6e6e973e0d2bb6fca203ee6", + "sha256:ef78c0d96098a3b5fe7720be4a97e73f439af7cf088ebf47b620aeaa10fadf97" ], "markers": "python_version >= '3.7'", - "version": "==2.5.3" + "version": "==2.5.5" }, "idna": { "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], "markers": "python_version >= '3.5'", - "version": "==3.3" + "version": "==3.4" }, "imagesize": { "hashes": [ @@ -2057,6 +2150,82 @@ "index": "pypi", "version": "==1.1.8" }, + "lxml": { + "hashes": [ + "sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318", + "sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c", + "sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b", + "sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000", + "sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73", + "sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d", + "sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb", + "sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8", + "sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2", + "sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345", + "sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94", + "sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e", + "sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b", + "sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc", + "sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a", + "sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9", + "sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc", + "sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387", + "sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb", + "sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7", + "sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4", + "sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97", + "sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67", + "sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627", + "sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7", + "sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd", + "sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3", + "sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7", + "sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130", + "sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b", + "sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036", + "sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785", + "sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca", + "sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91", + "sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc", + "sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536", + "sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391", + "sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3", + "sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d", + "sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21", + "sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3", + "sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d", + "sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29", + "sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715", + "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed", + "sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25", + "sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c", + "sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785", + "sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837", + "sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4", + "sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b", + "sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2", + "sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067", + "sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448", + "sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d", + "sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2", + "sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc", + "sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c", + "sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5", + "sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84", + "sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8", + "sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf", + "sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7", + "sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e", + "sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb", + "sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b", + "sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3", + "sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad", + "sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8", + "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f" + ], + "index": "pypi", + "version": "==4.9.1" + }, "markdown-it-py": { "hashes": [ "sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27", @@ -2113,44 +2282,50 @@ }, "matplotlib": { "hashes": [ - "sha256:0bcdfcb0f976e1bac6721d7d457c17be23cf7501f977b6a38f9d38a3762841f7", - "sha256:1e64ac9be9da6bfff0a732e62116484b93b02a0b4d4b19934fb4f8e7ad26ad6a", - "sha256:22227c976ad4dc8c5a5057540421f0d8708c6560744ad2ad638d48e2984e1dbc", - "sha256:2886cc009f40e2984c083687251821f305d811d38e3df8ded414265e4583f0c5", - "sha256:2e6d184ebe291b9e8f7e78bbab7987d269c38ea3e062eace1fe7d898042ef804", - "sha256:3211ba82b9f1518d346f6309df137b50c3dc4421b4ed4815d1d7eadc617f45a1", - "sha256:339cac48b80ddbc8bfd05daae0a3a73414651a8596904c2a881cfd1edb65f26c", - "sha256:35a8ad4dddebd51f94c5d24bec689ec0ec66173bf614374a1244c6241c1595e0", - "sha256:3b4fa56159dc3c7f9250df88f653f085068bcd32dcd38e479bba58909254af7f", - "sha256:43e9d3fa077bf0cc95ded13d331d2156f9973dce17c6f0c8b49ccd57af94dbd9", - "sha256:57f1b4e69f438a99bb64d7f2c340db1b096b41ebaa515cf61ea72624279220ce", - "sha256:5c096363b206a3caf43773abebdbb5a23ea13faef71d701b21a9c27fdcef72f4", - "sha256:6bb93a0492d68461bd458eba878f52fdc8ac7bdb6c4acdfe43dba684787838c2", - "sha256:6ea6aef5c4338e58d8d376068e28f80a24f54e69f09479d1c90b7172bad9f25b", - "sha256:6fe807e8a22620b4cd95cfbc795ba310dc80151d43b037257250faf0bfcd82bc", - "sha256:73dd93dc35c85dece610cca8358003bf0760d7986f70b223e2306b4ea6d1406b", - "sha256:839d47b8ead7ad9669aaacdbc03f29656dc21f0d41a6fea2d473d856c39c8b1c", - "sha256:874df7505ba820e0400e7091199decf3ff1fde0583652120c50cd60d5820ca9a", - "sha256:879c7e5fce4939c6aa04581dfe08d57eb6102a71f2e202e3314d5fbc072fd5a0", - "sha256:94ff86af56a3869a4ae26a9637a849effd7643858a1a04dd5ee50e9ab75069a7", - "sha256:99482b83ebf4eb6d5fc6813d7aacdefdd480f0d9c0b52dcf9f1cc3b2c4b3361a", - "sha256:9ab29589cef03bc88acfa3a1490359000c18186fc30374d8aa77d33cc4a51a4a", - "sha256:9befa5954cdbc085e37d974ff6053da269474177921dd61facdad8023c4aeb51", - "sha256:a206a1b762b39398efea838f528b3a6d60cdb26fe9d58b48265787e29cd1d693", - "sha256:ab8d26f07fe64f6f6736d635cce7bfd7f625320490ed5bfc347f2cdb4fae0e56", - "sha256:b28de401d928890187c589036857a270a032961411934bdac4cf12dde3d43094", - "sha256:b428076a55fb1c084c76cb93e68006f27d247169f056412607c5c88828d08f88", - "sha256:bf618a825deb6205f015df6dfe6167a5d9b351203b03fab82043ae1d30f16511", - "sha256:c995f7d9568f18b5db131ab124c64e51b6820a92d10246d4f2b3f3a66698a15b", - "sha256:cd45a6f3e93a780185f70f05cf2a383daed13c3489233faad83e81720f7ede24", - "sha256:d2484b350bf3d32cae43f85dcfc89b3ed7bd2bcd781ef351f93eb6fb2cc483f9", - "sha256:d62880e1f60e5a30a2a8484432bcb3a5056969dc97258d7326ad465feb7ae069", - "sha256:dacddf5bfcec60e3f26ec5c0ae3d0274853a258b6c3fc5ef2f06a8eb23e042be", - "sha256:f3840c280ebc87a48488a46f760ea1c0c0c83fcf7abbe2e6baf99d033fd35fd8", - "sha256:f814504e459c68118bf2246a530ed953ebd18213dc20e3da524174d84ed010b2" - ], - "index": "pypi", - "version": "==3.5.3" + "sha256:0958fc3fdc59c1b716ee1a5d14e73d03d541d873241a37c5c3a86f7ef6017923", + "sha256:0ae1b9b555212c1e242666af80e7ed796705869581e2d749971db4e682ccc1f3", + "sha256:11c1987b803cc2b26725659cfe817478f0a9597878e5c4bf374cfe4e12cbbd79", + "sha256:140316427a7c384e3dd37efb3a73cd67e14b0b237a6d277def91227f43cdcec2", + "sha256:1559213b803959a2b8309122585b5226d1c2fb66c933b1a2094cf1e99cb4fb90", + "sha256:16a899b958dd76606b571bc7eaa38f09160c27dfb262e493584644cfd4a77f0f", + "sha256:1739935d293d0348d7bf662e8cd0edb9c2aa8f20ccd646db755ce0f3456d24e4", + "sha256:1a4835c177821f3729be27ae9be7b8ae209fe75e83db7d9b2bfd319a998f0a42", + "sha256:2b60d4abcb6a405ca7d909c80791b00637d22c62aa3bb0ffff7e589f763867f5", + "sha256:2ed779a896b70c8012fe301fb91ee37e713e1dda1eb8f37de04cdbf506706983", + "sha256:3ec2edf7f74829eae287aa53d64d83ad5d43ee51d29fb1d88e689d8b36028312", + "sha256:408bbf968c15e9e38df9f25a588e372e28a43240cf5884c9bc6039a5021b7d5b", + "sha256:4699bb671dbc4afdb544eb893e4deb8a34e294b7734733f65b4fd2787ba5fbc6", + "sha256:4eba6972b796d97c8fcc5266b6dc42ef27c2dce4421b846cded0f3af851b81c9", + "sha256:51092d13499be72e47c15c3a1ae0209edaca6be42b65ffbbefbe0c85f6153c6f", + "sha256:62319d57dab5ad3e3494dd97a214e22079d3f72a0c8a2fd001829c2c6abbf8d1", + "sha256:657fb7712185f82211170ac4debae0800ed4f5992b8f7ebba2a9eabaf133a857", + "sha256:66a0db13f77aa7806dba29273874cf862450c61c2e5158245d17ee85d983fe8e", + "sha256:6b98e098549d3aea2bfb93f38f0b2ecadcb423fa1504bbff902c01efdd833fd8", + "sha256:7127e2b94571318531caf098dc9e8f60f5aba1704600f0b2483bf151d535674a", + "sha256:798559837156b8e2e2df97cffca748c5c1432af6ec5004c2932e475d813f1743", + "sha256:802feae98addb9f21707649a7f229c90a59fad34511881f20b906a5e8e6ea475", + "sha256:89e1978c3fbe4e3d4c6ad7db7e6f982607cb2546f982ccbe42708392437b1972", + "sha256:9295ca10a140c21e40d2ee43ef423213dc20767f6cea6b87c36973564bc51095", + "sha256:9711ef291e184b5a73c9d3af3f2d5cfe25d571c8dd95aa498415f74ac7e221a8", + "sha256:b0320f882214f6ffde5992081520b57b55450510bdaa020e96aacff9b7ae10e6", + "sha256:b5bd3b3ff191f81509d9a1afd62e1e3cda7a7889c35b5b6359a1241fe1511015", + "sha256:baa19508d8445f5648cd1ffe4fc6d4f7daf8b876f804e9a453df6c3708f6200b", + "sha256:c5108ebe67da60a9204497d8d403316228deb52b550388190c53a57394d41531", + "sha256:ccea337fb9a44866c5300c594b13d4d87e827ebc3c353bff15d298bac976b654", + "sha256:cd73a16a759865831be5a8fb6546f2a908c8d7d7f55c75f94ee7c2ca13cc95de", + "sha256:d840712f4b4c7d2a119f993d7e43ca9bcaa73aeaa24c322fa2bdf4f689a3ee09", + "sha256:df26a09d955b3ab9b6bc18658b9403ed839096c97d7abe8806194e228a485a3c", + "sha256:e01382c06ac3710155a0ca923047c5abe03c676d08f03e146c6a240d0a910713", + "sha256:e572c67958f7d55eae77f5f64dc7bd31968cc9f24c233926833efe63c60545f2", + "sha256:eca6f59cd0729edaeaa7032d582dffce518a420d4961ef3e8c93dce86be352c3", + "sha256:efd2e12f8964f8fb4ba1984df71d85d02ef0531e687e59f78ec8fc07271a3857", + "sha256:efe9e8037b989b14bb1887089ae763385431cc06fe488406413079cfd2a3a089", + "sha256:f0d5b9b14ccc7f539143ac9eb1c6b57d26d69ca52d30c3d719a7bc4123579e44", + "sha256:f1954d71cdf15c19e7f3bf2235a4fe1600ba42f34d472c9495bcf54d75a43e4e", + "sha256:fbbceb0a0dfe9213f6314510665a32ef25fe29b50657567cd00115fbfcb3b20d" + ], + "index": "pypi", + "version": "==3.6.0" }, "mdit-py-plugins": { "hashes": [ @@ -2178,32 +2353,12 @@ }, "mypy": { "hashes": [ - "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655", - "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9", - "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3", - "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6", - "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0", - "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58", - "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103", - "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09", - "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417", - "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56", - "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2", - "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856", - "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0", - "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8", - "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27", - "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5", - "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71", - "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27", - "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe", - "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca", - "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf", - "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9", - "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c" - ], - "index": "pypi", - "version": "==0.971" + "sha256:794f385653e2b749387a42afb1e14c2135e18daeb027e0d97162e4b7031210f8", + "sha256:ad77c13037d3402fbeffda07d51e3f228ba078d1c7096a73759c9419ea031bf4", + "sha256:fa38f82f53e1e7beb45557ff167c177802ba7b387ad017eab1663d567017c8ee" + ], + "index": "pypi", + "version": "==0.981" }, "mypy-extensions": { "hashes": [ @@ -2222,11 +2377,11 @@ }, "natsort": { "hashes": [ - "sha256:c7c1f3f27c375719a4dfcab353909fe39f26c2032a062a8c80cc844eaaca0445", - "sha256:f59988d2f24e77b6b56f8a8f882d5df6b3b637e09e075abc67b486d59fba1a4b" + "sha256:04fe18fdd2b9e5957f19f687eb117f102ef8dde6b574764e536e91194bed4f5f", + "sha256:57f85b72c688b09e053cdac302dd5b5b53df5f73ae20b4874fcbffd8bf783d11" ], "index": "pypi", - "version": "==8.1.0" + "version": "==8.2.0" }, "nodeenv": { "hashes": [ @@ -2238,37 +2393,37 @@ }, "numpy": { "hashes": [ - "sha256:17e5226674f6ea79e14e3b91bfbc153fdf3ac13f5cc54ee7bc8fdbe820a32da0", - "sha256:2bd879d3ca4b6f39b7770829f73278b7c5e248c91d538aab1e506c628353e47f", - "sha256:4f41f5bf20d9a521f8cab3a34557cd77b6f205ab2116651f12959714494268b0", - "sha256:5593f67e66dea4e237f5af998d31a43e447786b2154ba1ad833676c788f37cde", - "sha256:5e28cd64624dc2354a349152599e55308eb6ca95a13ce6a7d5679ebff2962913", - "sha256:633679a472934b1c20a12ed0c9a6c9eb167fbb4cb89031939bfd03dd9dbc62b8", - "sha256:806970e69106556d1dd200e26647e9bee5e2b3f1814f9da104a943e8d548ca38", - "sha256:806cc25d5c43e240db709875e947076b2826f47c2c340a5a2f36da5bb10c58d6", - "sha256:8247f01c4721479e482cc2f9f7d973f3f47810cbc8c65e38fd1bbd3141cc9842", - "sha256:8ebf7e194b89bc66b78475bd3624d92980fca4e5bb86dda08d677d786fefc414", - "sha256:8ecb818231afe5f0f568c81f12ce50f2b828ff2b27487520d85eb44c71313b9e", - "sha256:8f9d84a24889ebb4c641a9b99e54adb8cab50972f0166a3abc14c3b93163f074", - "sha256:909c56c4d4341ec8315291a105169d8aae732cfb4c250fbc375a1efb7a844f8f", - "sha256:9b83d48e464f393d46e8dd8171687394d39bc5abfe2978896b77dc2604e8635d", - "sha256:ac987b35df8c2a2eab495ee206658117e9ce867acf3ccb376a19e83070e69418", - "sha256:b78d00e48261fbbd04aa0d7427cf78d18401ee0abd89c7559bbf422e5b1c7d01", - "sha256:b8b97a8a87cadcd3f94659b4ef6ec056261fa1e1c3317f4193ac231d4df70215", - "sha256:bd5b7ccae24e3d8501ee5563e82febc1771e73bd268eef82a1e8d2b4d556ae66", - "sha256:bdc02c0235b261925102b1bd586579b7158e9d0d07ecb61148a1799214a4afd5", - "sha256:be6b350dfbc7f708d9d853663772a9310783ea58f6035eec649fb9c4371b5389", - "sha256:c403c81bb8ffb1c993d0165a11493fd4bf1353d258f6997b3ee288b0a48fce77", - "sha256:cf8c6aed12a935abf2e290860af8e77b26a042eb7f2582ff83dc7ed5f963340c", - "sha256:d98addfd3c8728ee8b2c49126f3c44c703e2b005d4a95998e2167af176a9e722", - "sha256:dc76bca1ca98f4b122114435f83f1fcf3c0fe48e4e6f660e07996abf2f53903c", - "sha256:dec198619b7dbd6db58603cd256e092bcadef22a796f778bf87f8592b468441d", - "sha256:df28dda02c9328e122661f399f7655cdcbcf22ea42daa3650a26bce08a187450", - "sha256:e603ca1fb47b913942f3e660a15e55a9ebca906857edfea476ae5f0fe9b457d5", - "sha256:ecfdd68d334a6b97472ed032b5b37a30d8217c097acfff15e8452c710e775524" - ], - "index": "pypi", - "version": "==1.23.2" + "sha256:004f0efcb2fe1c0bd6ae1fcfc69cc8b6bf2407e0f18be308612007a0762b4089", + "sha256:09f6b7bdffe57fc61d869a22f506049825d707b288039d30f26a0d0d8ea05164", + "sha256:0ea3f98a0ffce3f8f57675eb9119f3f4edb81888b6874bc1953f91e0b1d4f440", + "sha256:17c0e467ade9bda685d5ac7f5fa729d8d3e76b23195471adae2d6a6941bd2c18", + "sha256:1f27b5322ac4067e67c8f9378b41c746d8feac8bdd0e0ffede5324667b8a075c", + "sha256:22d43376ee0acd547f3149b9ec12eec2f0ca4a6ab2f61753c5b29bb3e795ac4d", + "sha256:2ad3ec9a748a8943e6eb4358201f7e1c12ede35f510b1a2221b70af4bb64295c", + "sha256:301c00cf5e60e08e04d842fc47df641d4a181e651c7135c50dc2762ffe293dbd", + "sha256:39a664e3d26ea854211867d20ebcc8023257c1800ae89773cbba9f9e97bae036", + "sha256:51bf49c0cd1d52be0a240aa66f3458afc4b95d8993d2d04f0d91fa60c10af6cd", + "sha256:78a63d2df1d947bd9d1b11d35564c2f9e4b57898aae4626638056ec1a231c40c", + "sha256:7cd1328e5bdf0dee621912f5833648e2daca72e3839ec1d6695e91089625f0b4", + "sha256:8355fc10fd33a5a70981a5b8a0de51d10af3688d7a9e4a34fcc8fa0d7467bb7f", + "sha256:8c79d7cf86d049d0c5089231a5bcd31edb03555bd93d81a16870aa98c6cfb79d", + "sha256:91b8d6768a75247026e951dce3b2aac79dc7e78622fc148329135ba189813584", + "sha256:94c15ca4e52671a59219146ff584488907b1f9b3fc232622b47e2cf832e94fb8", + "sha256:98dcbc02e39b1658dc4b4508442a560fe3ca5ca0d989f0df062534e5ca3a5c1a", + "sha256:a64403f634e5ffdcd85e0b12c08f04b3080d3e840aef118721021f9b48fc1460", + "sha256:bc6e8da415f359b578b00bcfb1d08411c96e9a97f9e6c7adada554a0812a6cc6", + "sha256:bdc9febce3e68b697d931941b263c59e0c74e8f18861f4064c1f712562903411", + "sha256:c1ba66c48b19cc9c2975c0d354f24058888cdc674bebadceb3cdc9ec403fb5d1", + "sha256:c9f707b5bb73bf277d812ded9896f9512a43edff72712f31667d0a8c2f8e71ee", + "sha256:d5422d6a1ea9b15577a9432e26608c73a78faf0b9039437b075cf322c92e98e7", + "sha256:e5d5420053bbb3dd64c30e58f9363d7a9c27444c3648e61460c1237f9ec3fa14", + "sha256:e868b0389c5ccfc092031a861d4e158ea164d8b7fdbb10e3b5689b4fc6498df6", + "sha256:efd9d3abe5774404becdb0748178b48a218f1d8c44e0375475732211ea47c67e", + "sha256:f8c02ec3c4c4fcb718fdf89a6c6f709b14949408e8cf2a2be5bfa9c49548fd85", + "sha256:ffcf105ecdd9396e05a8e58e81faaaf34d3f9875f137c7372450baa5d77c9a54" + ], + "index": "pypi", + "version": "==1.23.3" }, "opencv-python-headless": { "hashes": [ @@ -2293,30 +2448,36 @@ }, "pandas": { "hashes": [ - "sha256:07238a58d7cbc8a004855ade7b75bbd22c0db4b0ffccc721556bab8a095515f6", - "sha256:0daf876dba6c622154b2e6741f29e87161f844e64f84801554f879d27ba63c0d", - "sha256:16ad23db55efcc93fa878f7837267973b61ea85d244fc5ff0ccbcfa5638706c5", - "sha256:1d9382f72a4f0e93909feece6fef5500e838ce1c355a581b3d8f259839f2ea76", - "sha256:24ea75f47bbd5574675dae21d51779a4948715416413b30614c1e8b480909f81", - "sha256:2893e923472a5e090c2d5e8db83e8f907364ec048572084c7d10ef93546be6d1", - "sha256:2ff7788468e75917574f080cd4681b27e1a7bf36461fe968b49a87b5a54d007c", - "sha256:41fc406e374590a3d492325b889a2686b31e7a7780bec83db2512988550dadbf", - "sha256:48350592665ea3cbcd07efc8c12ff12d89be09cd47231c7925e3b8afada9d50d", - "sha256:605d572126eb4ab2eadf5c59d5d69f0608df2bf7bcad5c5880a47a20a0699e3e", - "sha256:6dfbf16b1ea4f4d0ee11084d9c026340514d1d30270eaa82a9f1297b6c8ecbf0", - "sha256:6f803320c9da732cc79210d7e8cc5c8019aad512589c910c66529eb1b1818230", - "sha256:721a3dd2f06ef942f83a819c0f3f6a648b2830b191a72bbe9451bcd49c3bd42e", - "sha256:755679c49460bd0d2f837ab99f0a26948e68fa0718b7e42afbabd074d945bf84", - "sha256:78b00429161ccb0da252229bcda8010b445c4bf924e721265bec5a6e96a92e92", - "sha256:958a0588149190c22cdebbc0797e01972950c927a11a900fe6c2296f207b1d6f", - "sha256:a3924692160e3d847e18702bb048dc38e0e13411d2b503fecb1adf0fcf950ba4", - "sha256:d51674ed8e2551ef7773820ef5dab9322be0828629f2cbf8d1fc31a0c4fed640", - "sha256:d5ebc990bd34f4ac3c73a2724c2dcc9ee7bf1ce6cf08e87bb25c6ad33507e318", - "sha256:d6c0106415ff1a10c326c49bc5dd9ea8b9897a6ca0c8688eb9c30ddec49535ef", - "sha256:e48fbb64165cda451c06a0f9e4c7a16b534fcabd32546d531b3c240ce2844112" - ], - "index": "pypi", - "version": "==1.4.3" + "sha256:0d8d7433d19bfa33f11c92ad9997f15a902bda4f5ad3a4814a21d2e910894484", + "sha256:1642fc6138b4e45d57a12c1b464a01a6d868c0148996af23f72dde8d12486bbc", + "sha256:171cef540bfcec52257077816a4dbbac152acdb8236ba11d3196ae02bf0959d8", + "sha256:1b82ccc7b093e0a93f8dffd97a542646a3e026817140e2c01266aaef5fdde11b", + "sha256:1d34b1f43d9e3f4aea056ba251f6e9b143055ebe101ed04c847b41bb0bb4a989", + "sha256:207d63ac851e60ec57458814613ef4b3b6a5e9f0b33c57623ba2bf8126c311f8", + "sha256:2504c032f221ef9e4a289f5e46a42b76f5e087ecb67d62e342ccbba95a32a488", + "sha256:33a9d9e21ab2d91e2ab6e83598419ea6a664efd4c639606b299aae8097c1c94f", + "sha256:3ee61b881d2f64dd90c356eb4a4a4de75376586cd3c9341c6c0fcaae18d52977", + "sha256:41aec9f87455306496d4486df07c1b98c15569c714be2dd552a6124cd9fda88f", + "sha256:4e30a31039574d96f3d683df34ccb50bb435426ad65793e42a613786901f6761", + "sha256:5cc47f2ebaa20ef96ae72ee082f9e101b3dfbf74f0e62c7a12c0b075a683f03c", + "sha256:62e61003411382e20d7c2aec1ee8d7c86c8b9cf46290993dd8a0a3be44daeb38", + "sha256:73844e247a7b7dac2daa9df7339ecf1fcf1dfb8cbfd11e3ffe9819ae6c31c515", + "sha256:85a516a7f6723ca1528f03f7851fa8d0360d1d6121cf15128b290cf79b8a7f6a", + "sha256:86d87279ebc5bc20848b4ceb619073490037323f80f515e0ec891c80abad958a", + "sha256:8a4fc04838615bf0a8d3a03ed68197f358054f0df61f390bcc64fbe39e3d71ec", + "sha256:8e8e5edf97d8793f51d258c07c629bd49d271d536ce15d66ac00ceda5c150eb3", + "sha256:947ed9f896ee61adbe61829a7ae1ade493c5a28c66366ec1de85c0642009faac", + "sha256:a68a9b9754efff364b0c5ee5b0f18e15ca640c01afe605d12ba8b239ca304d6b", + "sha256:c76f1d104844c5360c21d2ef0e1a8b2ccf8b8ebb40788475e255b9462e32b2be", + "sha256:c7f38d91f21937fe2bec9449570d7bf36ad7136227ef43b321194ec249e2149d", + "sha256:de34636e2dc04e8ac2136a8d3c2051fd56ebe9fd6cd185581259330649e73ca9", + "sha256:e178ce2d7e3b934cf8d01dc2d48d04d67cb0abfaffdcc8aa6271fd5a436f39c8", + "sha256:e252a9e49b233ff96e2815c67c29702ac3a062098d80a170c506dff3470fd060", + "sha256:e9c5049333c5bebf993033f4bf807d163e30e8fada06e1da7fa9db86e2392009", + "sha256:fc987f7717e53d372f586323fff441263204128a1ead053c1b98d7288f836ac9" + ], + "index": "pypi", + "version": "==1.5.0" }, "parameterized": { "hashes": [ @@ -2351,6 +2512,7 @@ "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f", "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069", "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402", + "sha256:336b9036127eab855beec9662ac3ea13a4544a523ae273cbf108b228ecac8437", "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885", "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e", "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be", @@ -2379,6 +2541,7 @@ "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8", "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb", "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3", + "sha256:adabc0bce035467fb537ef3e5e74f2847c8af217ee0be0455d4fec8adc0462fc", "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf", "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1", "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a", @@ -2556,11 +2719,11 @@ }, "pytest": { "hashes": [ - "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c", - "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45" + "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7", + "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39" ], "index": "pypi", - "version": "==7.1.2" + "version": "==7.1.3" }, "pytest-forked": { "hashes": [ @@ -2595,6 +2758,7 @@ }, "pyyaml": { "hashes": [ + "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", @@ -2606,26 +2770,32 @@ "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" ], @@ -2678,11 +2848,11 @@ }, "setuptools": { "hashes": [ - "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82", - "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57" + "sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9", + "sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1" ], "markers": "python_version >= '3.7'", - "version": "==65.3.0" + "version": "==65.4.0" }, "six": { "hashes": [ @@ -2708,11 +2878,11 @@ }, "sphinx": { "hashes": [ - "sha256:309a8da80cb6da9f4713438e5b55861877d5d7976b69d87e336733637ea12693", - "sha256:ba3224a4e206e1fbdecf98a4fae4992ef9b24b85ebf7b584bb340156eaf08d89" + "sha256:3dcf00fcf82cf91118db9b7177edea4fc01998976f893928d0ab0c58c54be2ca", + "sha256:c009bb2e9ac5db487bcf53f015504005a330ff7c631bb6ab2604e0d65bae8b54" ], "index": "pypi", - "version": "==5.1.1" + "version": "==5.2.1" }, "sphinx-rtd-theme": { "hashes": [ @@ -2797,11 +2967,11 @@ }, "tenacity": { "hashes": [ - "sha256:43242a20e3e73291a28bcbcacfd6e000b02d3857a9a9fff56b297a27afdc932f", - "sha256:f78f4ea81b0fabc06728c11dc2a8c01277bfc5181b321a4770471902e3eb844a" + "sha256:35525cd47f82830069f0d6b73f7eb83bc5b73ee2fff0437952cedf98b27653ac", + "sha256:e48c437fdf9340f5666b92cd7990e96bc5fc955e1298baf4a907e3972067a445" ], "index": "pypi", - "version": "==8.0.1" + "version": "==8.1.0" }, "toml": { "hashes": [ @@ -2819,6 +2989,53 @@ "markers": "python_version < '3.11'", "version": "==2.0.1" }, + "types-atomicwrites": { + "hashes": [ + "sha256:458a985b2cfaa963becec21ba63faaa5dd241e237ba8bf024732b37b18690de6", + "sha256:ef824f7e639c178e3e571f60d228c7745198756ebfcc5d249a0e3e02e04b2858" + ], + "index": "pypi", + "version": "==1.4.5" + }, + "types-certifi": { + "hashes": [ + "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f", + "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a" + ], + "index": "pypi", + "version": "==2021.10.8.3" + }, + "types-pycurl": { + "hashes": [ + "sha256:82e00aa2981595bfa55e5a3bac42221eb3435b0026dffbe1177f6ac9f2d51200", + "sha256:9eab3414da4a1b1e9a628bd288fc5172b8c182e1d9fb6d8d082441b0fd64baed" + ], + "index": "pypi", + "version": "==7.45.1" + }, + "types-pyyaml": { + "hashes": [ + "sha256:7f7da2fd11e9bc1e5e9eb3ea1be84f4849747017a59fc2eee0ea34ed1147c2e0", + "sha256:8f890028123607379c63550179ddaec4517dc751f4c527a52bb61934bf495989" + ], + "index": "pypi", + "version": "==6.0.11" + }, + "types-requests": { + "hashes": [ + "sha256:7ee827eb8ce611b02b5117cfec5da6455365b6a575f5e3ff19f655ba603e6b4e", + "sha256:af5f55e803cabcfb836dad752bd6d8a0fc8ef1cd84243061c0e27dee04ccf4fd" + ], + "index": "pypi", + "version": "==2.28.11" + }, + "types-urllib3": { + "hashes": [ + "sha256:a1b3aaea7dda3eb1b51699ee723aadd235488e4dc4648e030f09bc429ecff42f", + "sha256:cf7918503d02d3576e503bbfb419b0e047c4617653bba09624756ab7175e15c9" + ], + "version": "==1.26.24" + }, "typing-extensions": { "hashes": [ "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", @@ -2837,11 +3054,11 @@ }, "virtualenv": { "hashes": [ - "sha256:4193b7bc8a6cd23e4eb251ac64f29b4398ab2c233531e66e40b19a6b7b0d30c1", - "sha256:d86ea0bb50e06252d79e6c241507cb904fcd66090c3271381372d6221a3970f9" + "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da", + "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27" ], "markers": "python_version >= '3.6'", - "version": "==20.16.3" + "version": "==20.16.5" }, "zipp": { "hashes": [ diff --git a/RELEASES.md b/RELEASES.md index dcea1e72cc..35ed1b9520 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -3,13 +3,14 @@ Version 0.8.17 (2022-XX-XX) * New driving model * Internal feature space accuracy increased tenfold during training, this makes the model dramatically more accurate. * Self-tuning torque lateral controller parameters - * Parameters are learned live for each car + * Parameters learned live for each car * Enabled only on Toyota Corolla for now * UI updates + * Multi-language in navigation * Matched speeds shown on car's dash * Improved update experience * Border turns grey while overriding steering - * Added button to flag events that are shown in comma connect + * Added button to bookmark events while driving; view them later in comma connect * AGNOS 6 Version 0.8.16 (2022-08-26) diff --git a/SConstruct b/SConstruct index 178b0cc872..23ab37dc1e 100644 --- a/SConstruct +++ b/SConstruct @@ -431,7 +431,12 @@ SConscript(['selfdrive/sensord/SConscript']) SConscript(['selfdrive/ui/SConscript']) SConscript(['selfdrive/navd/SConscript']) -SConscript(['tools/replay/SConscript']) +if arch in ['x86_64', 'Darwin'] or GetOption('extras'): + SConscript(['tools/replay/SConscript']) + + opendbc = abspath([File('opendbc/can/libdbc.so')]) + Export('opendbc') + SConscript(['tools/cabana/SConscript']) if GetOption('test'): SConscript('panda/tests/safety/SConscript') diff --git a/cereal b/cereal index 3baa20e1da..5766e645f2 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 3baa20e1da5d88dcb1d3ae9678471eb8013958f2 +Subproject commit 5766e645f2ee2a131b145fb1ea9e3b7c55a4a740 diff --git a/common/modeldata.h b/common/modeldata.h index e13840d53e..a00d3d49d3 100644 --- a/common/modeldata.h +++ b/common/modeldata.h @@ -4,7 +4,7 @@ #include "common/mat.h" #include "system/hardware/hw.h" -const int TRAJECTORY_SIZE = 33; +const int TRAJECTORY_SIZE = 33; const int LAT_MPC_N = 16; const int LON_MPC_N = 32; const float MIN_DRAW_DISTANCE = 10.0; diff --git a/common/params.cc b/common/params.cc index 63208879b2..155bc88487 100644 --- a/common/params.cc +++ b/common/params.cc @@ -118,6 +118,7 @@ std::unordered_map keys = { {"GithubUsername", PERSISTENT}, {"GitRemote", PERSISTENT}, {"GsmApn", PERSISTENT}, + {"GsmMetered", PERSISTENT}, {"GsmRoaming", PERSISTENT}, {"HardwareSerial", PERSISTENT}, {"HasAcceptedTerms", PERSISTENT}, @@ -165,6 +166,7 @@ std::unordered_map keys = { {"TermsVersion", PERSISTENT}, {"Timezone", PERSISTENT}, {"TrainingVersion", PERSISTENT}, + {"UbloxAvailable", PERSISTENT}, {"UpdateAvailable", CLEAR_ON_MANAGER_START}, {"UpdateFailedCount", CLEAR_ON_MANAGER_START}, {"UpdaterState", CLEAR_ON_MANAGER_START}, @@ -297,10 +299,13 @@ std::map Params::readAll() { void Params::clearAll(ParamKeyType key_type) { FileLock file_lock(params_path + "/.lock"); - std::string path; - for (auto &[key, type] : keys) { - if (type & key_type) { - unlink(getParamPath(key).c_str()); + if (key_type == ALL) { + util::remove_files_in_dir(getParamPath()); + } else { + for (auto &[key, type] : keys) { + if (type & key_type) { + unlink(getParamPath(key).c_str()); + } } } diff --git a/common/params.h b/common/params.h index 7758a015f6..aecb3ee471 100644 --- a/common/params.h +++ b/common/params.h @@ -29,8 +29,8 @@ public: // helpers for reading values std::string get(const std::string &key, bool block = false); - inline bool getBool(const std::string &key) { - return get(key) == "1"; + inline bool getBool(const std::string &key, bool block = false) { + return get(key, block) == "1"; } std::map readAll(); diff --git a/common/params_pyx.pyx b/common/params_pyx.pyx index bbddda46ea..9d8933609f 100755 --- a/common/params_pyx.pyx +++ b/common/params_pyx.pyx @@ -16,7 +16,7 @@ cdef extern from "common/params.h": cdef cppclass c_Params "Params": c_Params(string) nogil string get(string, bool) nogil - bool getBool(string) nogil + bool getBool(string, bool) nogil int remove(string) nogil int put(string, string) nogil int putBool(string, bool) nogil @@ -68,11 +68,11 @@ cdef class Params: return val if encoding is None else val.decode(encoding) - def get_bool(self, key): + def get_bool(self, key, bool block=False): cdef string k = self.check_key(key) cdef bool r with nogil: - r = self.p.getBool(k) + r = self.p.getBool(k, block) return r def put(self, key, dat): diff --git a/common/realtime.py b/common/realtime.py index 8a79d8d39f..7dd2eb98a6 100644 --- a/common/realtime.py +++ b/common/realtime.py @@ -31,7 +31,7 @@ class Priority: def set_realtime_priority(level: int) -> None: if not PC: - os.sched_setscheduler(0, os.SCHED_FIFO, os.sched_param(level)) # type: ignore[attr-defined] # pylint: disable=no-member + os.sched_setscheduler(0, os.SCHED_FIFO, os.sched_param(level)) # pylint: disable=no-member def set_core_affinity(cores: List[int]) -> None: diff --git a/common/tests/test_util.cc b/common/tests/test_util.cc index 25ecf09aa9..b70cc9044a 100644 --- a/common/tests/test_util.cc +++ b/common/tests/test_util.cc @@ -143,3 +143,20 @@ TEST_CASE("util::create_directories") { REQUIRE(util::create_directories("", 0755) == false); } } + +TEST_CASE("util::remove_files_in_dir") { + std::string tmp_dir = "/tmp/test_remove_all_in_dir"; + system("rm /tmp/test_remove_all_in_dir -rf"); + REQUIRE(util::create_directories(tmp_dir, 0755)); + const int tmp_file_cnt = 10; + for (int i = 0; i < tmp_file_cnt; ++i) { + std::string tmp_file = tmp_dir + "/test_XXXXXX"; + int fd = mkstemp((char*)tmp_file.c_str()); + close(fd); + REQUIRE(util::file_exists(tmp_file.c_str())); + } + + REQUIRE(util::read_files_in_dir(tmp_dir).size() == tmp_file_cnt); + util::remove_files_in_dir(tmp_dir); + REQUIRE(util::read_files_in_dir(tmp_dir).empty()); +} diff --git a/common/util.cc b/common/util.cc index 92add63997..b6a8322a27 100644 --- a/common/util.cc +++ b/common/util.cc @@ -97,6 +97,22 @@ std::map read_files_in_dir(const std::string &path) { return ret; } +void remove_files_in_dir(const std::string &path) { + DIR *d = opendir(path.c_str()); + if (!d) return; + + std::string fn; + struct dirent *de = NULL; + while ((de = readdir(d))) { + if (de->d_type != DT_DIR) { + fn = path + "/" + de->d_name; + unlink(fn.c_str()); + } + } + + closedir(d); +} + int write_file(const char* path, const void* data, size_t size, int flags, mode_t mode) { int fd = HANDLE_EINTR(open(path, flags, mode)); if (fd == -1) { diff --git a/common/util.h b/common/util.h index f3a24723b4..e13f4dc130 100644 --- a/common/util.h +++ b/common/util.h @@ -80,6 +80,7 @@ std::string dir_name(std::string const& path); // **** file fhelpers ***** std::string read_file(const std::string& fn); std::map read_files_in_dir(const std::string& path); +void remove_files_in_dir(const std::string& path); int write_file(const char* path, const void* data, size_t size, int flags = O_WRONLY, mode_t mode = 0664); FILE* safe_fopen(const char* filename, const char* mode); diff --git a/docs/CARS.md b/docs/CARS.md index ecd6932e87..40eef06102 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 three. All supported cars provide a better experience than any stock system. -# 204 Supported Cars +# 208 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Harness| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:| @@ -55,9 +55,10 @@ A supported vehicle is one that just works when you install a comma three. All s |Honda|Ridgeline 2017-22|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Nidec| |Hyundai|Elantra 2017-19|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai B| |Hyundai|Elantra 2021-22|Smart Cruise Control (SCC)|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai K| -|Hyundai|Elantra Hybrid 2021-22|Smart Cruise Control (SCC)|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai K| +|Hyundai|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai K| |Hyundai|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai J| -|Hyundai|Ioniq 5 2022|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai Q| +|Hyundai|Ioniq 5 (with HDA II) 2022|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai Q| +|Hyundai|Ioniq 5 (without HDA II) 2022|Highway Driving Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai K| |Hyundai|Ioniq Electric 2019|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai C| |Hyundai|Ioniq Electric 2020|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H| |Hyundai|Ioniq Hybrid 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai C| @@ -83,7 +84,8 @@ A supported vehicle is one that just works when you install a comma three. All s |Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|FCA| |Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|FCA| |Kia|Ceed 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai E| -|Kia|EV6 2022|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai P| +|Kia|EV6 (with HDA II) 2022|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai P| +|Kia|EV6 (without HDA II) 2022|Highway Driving Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai L| |Kia|Forte 2019-21|Smart Cruise Control (SCC)|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai G| |Kia|K5 2021-22|Smart Cruise Control (SCC)|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai A| |Kia|Niro EV 2019|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H| @@ -110,9 +112,11 @@ A supported vehicle is one that just works when you install a comma three. All s |Lexus|NX Hybrid 2018-19|All|Stock[3](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota| |Lexus|NX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Lexus|RC 2017-20|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota| -|Lexus|RX 2016-19|All|Stock[3](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota| +|Lexus|RX 2016|Lexus Safety System+|Stock[3](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota| +|Lexus|RX 2017-19|All|Stock[3](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota| |Lexus|RX 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| -|Lexus|RX Hybrid 2016-19|All|Stock[3](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota| +|Lexus|RX Hybrid 2016|Lexus Safety System+|Stock[3](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota| +|Lexus|RX Hybrid 2017-19|All|Stock[3](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota| |Lexus|RX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Lexus|UX Hybrid 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Mazda|CX-5 2022|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Mazda| diff --git a/laika_repo b/laika_repo index c8bc1fa01b..e1049cde0a 160000 --- a/laika_repo +++ b/laika_repo @@ -1 +1 @@ -Subproject commit c8bc1fa01be9f22592efb991ee52d3d965d21968 +Subproject commit e1049cde0a68f7d4a70b1ebd76befdc0e163ad55 diff --git a/launch_env.sh b/launch_env.sh index 48c5696b94..88e1f2a9c5 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="6" + export AGNOS_VERSION="6.1" fi if [ -z "$PASSIVE" ]; then diff --git a/mypy.ini b/mypy.ini index e2da60f926..39b1b007a7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,4 +1,16 @@ [mypy] python_version = 3.8 -ignore_missing_imports = True plugins = numpy.typing.mypy_plugin +files = body, common, docs, scripts, selfdrive, site_scons, system, tools +exclude = ^(pyextra/)|(cereal/)|(opendbc/)|(panda/)|(laika/)|(laika_repo/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(xx/) + +; third-party packages +ignore_missing_imports = True + +; helpful warnings +warn_redundant_casts = True +warn_unreachable = True +warn_unused_ignores = True + +; restrict dynamic typing +warn_return_any = True diff --git a/opendbc b/opendbc index eaac172af9..dde0ff6f44 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit eaac172af9cb342204e69ec52339cdf3c6a8ac4e +Subproject commit dde0ff6f4456c167df204bf5ac1e3f2979c844c9 diff --git a/release/build_devel.sh b/release/build_devel.sh index f06e3102c8..668ac0de19 100755 --- a/release/build_devel.sh +++ b/release/build_devel.sh @@ -12,10 +12,7 @@ fi # set git identity source $DIR/identity.sh -echo "[-] Setting up repo T=$SECONDS" - -cd $SOURCE_DIR -git fetch origin +echo "[-] Setting up target repo T=$SECONDS" rm -rf $TARGET_DIR mkdir -p $TARGET_DIR @@ -25,14 +22,14 @@ pre-commit uninstall || true echo "[-] bringing master-ci and devel in sync T=$SECONDS" cd $TARGET_DIR -git fetch origin master-ci -git fetch origin devel +git fetch --depth 1 origin master-ci +git fetch --depth 1 origin devel git checkout -f --track origin/master-ci git reset --hard master-ci git checkout master-ci git reset --hard origin/devel -git clean -xdf +git clean -xdff git lfs uninstall # remove everything except .git @@ -41,7 +38,7 @@ find . -maxdepth 1 -not -path './.git' -not -name '.' -not -name '..' -exec rm - # reset source tree cd $SOURCE_DIR -git clean -xdf +git clean -xdff # do the files copy echo "[-] copying files T=$SECONDS" diff --git a/release/files_common b/release/files_common index 5783edd070..07ffaf8501 100644 --- a/release/files_common +++ b/release/files_common @@ -305,6 +305,8 @@ selfdrive/ui/soundd/soundd selfdrive/ui/soundd/.gitignore selfdrive/ui/translations/*.ts selfdrive/ui/translations/languages.json +selfdrive/ui/update_translations.py +selfdrive/ui/tests/test_translations.py selfdrive/ui/qt/*.cc selfdrive/ui/qt/*.h @@ -572,3 +574,4 @@ tinygrad_repo/tinygrad/ops.py tinygrad_repo/tinygrad/shapetracker.py tinygrad_repo/tinygrad/tensor.py tinygrad_repo/tinygrad/nn/__init__.py +tinygrad_repo/tinygrad/nn/optim.py diff --git a/scripts/launch_corolla.sh b/scripts/launch_corolla.sh index 0801938e71..146fbacf0a 100755 --- a/scripts/launch_corolla.sh +++ b/scripts/launch_corolla.sh @@ -3,4 +3,5 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" export FINGERPRINT="TOYOTA COROLLA TSS2 2019" +export SKIP_FW_QUERY="1" $DIR/../launch_openpilot.sh diff --git a/scripts/switch_to_master.sh b/scripts/switch_to_master.sh deleted file mode 100755 index cad51eb549..0000000000 --- a/scripts/switch_to_master.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/bash - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -cd $DIR/.. - -git clean -xdf . -git rm -r --cached . - -git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" -git fetch origin master -git checkout master -git reset --hard -git submodule update --init - -printf '\n\n' -echo "master checked out. reboot to start building openpilot master" diff --git a/selfdrive/athena/tests/helpers.py b/selfdrive/athena/tests/helpers.py index 071393cb14..a43527c260 100644 --- a/selfdrive/athena/tests/helpers.py +++ b/selfdrive/athena/tests/helpers.py @@ -53,8 +53,8 @@ class MockParams(): default_params = { "DongleId": b"0000000000000000", "GithubSshKeys": b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC307aE+nuHzTAgaJhzSf5v7ZZQW9gaperjhCmyPyl4PzY7T1mDGenTlVTN7yoVFZ9UfO9oMQqo0n1OwDIiqbIFxqnhrHU0cYfj88rI85m5BEKlNu5RdaVTj1tcbaPpQc5kZEolaI1nDDjzV0lwS7jo5VYDHseiJHlik3HH1SgtdtsuamGR2T80q1SyW+5rHoMOJG73IH2553NnWuikKiuikGHUYBd00K1ilVAK2xSiMWJp55tQfZ0ecr9QjEsJ+J/efL4HqGNXhffxvypCXvbUYAFSddOwXUPo5BTKevpxMtH+2YrkpSjocWA04VnTYFiPG6U4ItKmbLOTFZtPzoez private", # noqa: E501 + "GsmMetered": True, "AthenadUploadQueue": '[]', - "CellularUnmetered": False, } params = default_params.copy() diff --git a/selfdrive/boardd/boardd.cc b/selfdrive/boardd/boardd.cc index 9819944af8..2d613b68ce 100644 --- a/selfdrive/boardd/boardd.cc +++ b/selfdrive/boardd/boardd.cc @@ -56,7 +56,6 @@ using namespace std::chrono_literals; std::atomic ignition(false); -std::atomic pigeon_active(false); ExitHandler do_exit; @@ -114,8 +113,9 @@ bool safety_setter_thread(std::vector pandas) { } // set to ELM327 for fingerprinting - for (Panda *panda : pandas) { - panda->set_safety_model(cereal::CarParams::SafetyModel::ELM327); + for (int i = 0; i < pandas.size(); i++) { + const uint16_t safety_param = (i > 0) ? 1U : 0U; + pandas[i]->set_safety_model(cereal::CarParams::SafetyModel::ELM327, safety_param); } Params p = Params(); @@ -346,7 +346,7 @@ std::optional send_panda_states(PubMaster *pm, const std::vector } #ifndef __x86_64__ - bool power_save_desired = !ignition_local && !pigeon_active; + bool power_save_desired = !ignition_local; if (health.power_save_enabled_pkt != power_save_desired) { panda->set_power_saving(power_save_desired); } @@ -382,6 +382,7 @@ std::optional send_panda_states(PubMaster *pm, const std::vector ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt)); ps.setInterruptLoad(health.interrupt_load); ps.setFanPower(health.fan_power); + ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid)); std::array cs = {ps.initCanState0(), ps.initCanState1(), ps.initCanState2()}; @@ -407,6 +408,7 @@ std::optional send_panda_states(PubMaster *pm, const std::vector cs[j].setCanDataSpeed(can_health.can_data_speed); cs[j].setCanfdEnabled(can_health.canfd_enabled); cs[j].setBrsEnabled(can_health.brs_enabled); + cs[j].setCanfdNonIso(can_health.canfd_non_iso); } // Convert faults bitset to capnp list diff --git a/selfdrive/boardd/panda.cc b/selfdrive/boardd/panda.cc index 7ddf15f9ce..0b8630b0c0 100644 --- a/selfdrive/boardd/panda.cc +++ b/selfdrive/boardd/panda.cc @@ -360,6 +360,10 @@ void Panda::set_data_speed_kbps(uint16_t bus, uint16_t speed) { usb_write(0xf9, bus, (speed * 10)); } +void Panda::set_canfd_non_iso(uint16_t bus, bool non_iso) { + usb_write(0xfc, bus, non_iso); +} + static uint8_t len_to_dlc(uint8_t len) { if (len <= 8) { return len; diff --git a/selfdrive/boardd/panda.h b/selfdrive/boardd/panda.h index a4afbdac1a..c7a0e7a6c1 100644 --- a/selfdrive/boardd/panda.h +++ b/selfdrive/boardd/panda.h @@ -91,6 +91,7 @@ class Panda { void send_heartbeat(bool engaged); void set_can_speed_kbps(uint16_t bus, uint16_t speed); void set_data_speed_kbps(uint16_t bus, uint16_t speed); + void set_canfd_non_iso(uint16_t bus, bool non_iso); void can_send(capnp::List::Reader can_data_list); bool can_receive(std::vector& out_vec); diff --git a/selfdrive/car/__init__.py b/selfdrive/car/__init__.py index 8ff39ceaeb..491c551b1b 100644 --- a/selfdrive/car/__init__.py +++ b/selfdrive/car/__init__.py @@ -3,7 +3,7 @@ import capnp from cereal import car from common.numpy_fast import clip -from typing import Dict, List +from typing import Dict # kg of standard extra cargo to count for drive, gas, etc... STD_CARGO_KG = 136. @@ -32,19 +32,6 @@ def create_button_event(cur_but: int, prev_but: int, buttons_dict: Dict[int, cap return be -def create_button_enable_events(buttonEvents: capnp.lib.capnp._DynamicListBuilder, pcm_cruise: bool = False) -> List[int]: - events = [] - for b in buttonEvents: - # do enable on both accel and decel buttons - if not pcm_cruise: - if b.type in (ButtonType.accelCruise, ButtonType.decelCruise) and not b.pressed: - events.append(EventName.buttonEnable) - # do disable on button down - if b.type == ButtonType.cancel and b.pressed: - events.append(EventName.buttonCancel) - return events - - def gen_empty_fingerprint(): return {i: {} for i in range(0, 8)} diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index 4f098fadb5..4a8fd5fbd9 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -81,7 +81,7 @@ def fingerprint(logcan, sendcan): skip_fw_query = os.environ.get('SKIP_FW_QUERY', False) ecu_rx_addrs = set() - if not fixed_fingerprint and not skip_fw_query: + if not skip_fw_query: # Vin query only reliably works through OBDII bus = 1 @@ -95,16 +95,19 @@ def fingerprint(logcan, sendcan): cloudlog.warning("Using cached CarParams") vin, vin_rx_addr = cached_params.carVin, 0 car_fw = list(cached_params.carFw) + cached = True else: cloudlog.warning("Getting VIN & FW versions") - _, vin_rx_addr, vin = get_vin(logcan, sendcan, bus) + vin_rx_addr, vin = get_vin(logcan, sendcan, bus) ecu_rx_addrs = get_present_ecus(logcan, sendcan) car_fw = get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs) + cached = False exact_fw_match, fw_candidates = match_fw_to_car(car_fw) else: vin, vin_rx_addr = VIN_UNKNOWN, 0 exact_fw_match, fw_candidates, car_fw = True, set(), [] + cached = False if not is_valid_vin(vin): cloudlog.event("Malformed VIN", vin=vin, error=True) @@ -165,7 +168,7 @@ def fingerprint(logcan, sendcan): car_fingerprint = fixed_fingerprint source = car.CarParams.FingerprintSource.fixed - cloudlog.event("fingerprinted", car_fingerprint=car_fingerprint, source=source, fuzzy=not exact_match, + cloudlog.event("fingerprinted", car_fingerprint=car_fingerprint, source=source, fuzzy=not exact_match, cached=cached, fw_count=len(car_fw), ecu_responses=list(ecu_rx_addrs), vin_rx_addr=vin_rx_addr, error=True) return car_fingerprint, finger, vin, car_fw, source, exact_match diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index 7180ace524..c703ef6cb8 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -166,14 +166,13 @@ FW_QUERY_CONFIG = FwQueryConfig( bus=0, ), ], + extra_ecus=[ + (Ecu.hcp, 0x7e2, None), # manages transmission on hybrids + (Ecu.abs, 0x7e4, None), # alt address for abs on hybrids + ], ) FW_VERSIONS = { - CAR.PACIFICA_2019_HYBRID: { - (Ecu.hcp, 0x7e2, None): [], - (Ecu.abs, 0x7e4, None): [], - }, - CAR.RAM_1500: { (Ecu.combinationMeter, 0x742, None): [ b'68294063AH', diff --git a/selfdrive/car/ecu_addrs.py b/selfdrive/car/ecu_addrs.py index 267701509a..9f6ace2b5f 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 -import traceback from typing import Optional, Set, Tuple import cereal.messaging as messaging @@ -62,7 +61,7 @@ def get_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, que print(f"Duplicate ECU address: {hex(msg.address)}") ecu_responses.add((msg.address, subaddr, msg.src)) except Exception: - cloudlog.warning(f"ECU addr scan exception: {traceback.format_exc()}") + cloudlog.exception("ECU addr scan exception") return ecu_responses @@ -71,6 +70,8 @@ if __name__ == "__main__": parser = argparse.ArgumentParser(description='Get addresses of all ECUs') parser.add_argument('--debug', action='store_true') + parser.add_argument('--bus', type=int, default=1) + parser.add_argument('--timeout', type=float, default=1.0) args = parser.parse_args() logcan = messaging.sub_sock('can') @@ -79,7 +80,7 @@ if __name__ == "__main__": time.sleep(1.0) print("Getting ECU addresses ...") - ecu_addrs = get_all_ecu_addrs(logcan, sendcan, 1, debug=args.debug) + ecu_addrs = get_all_ecu_addrs(logcan, sendcan, args.bus, args.timeout, debug=args.debug) print() print("Found ECUs on addresses:") diff --git a/selfdrive/car/ford/carcontroller.py b/selfdrive/car/ford/carcontroller.py index 592d8586ca..f18014601c 100644 --- a/selfdrive/car/ford/carcontroller.py +++ b/selfdrive/car/ford/carcontroller.py @@ -3,7 +3,7 @@ from cereal import car from common.numpy_fast import clip, interp from opendbc.can.packer import CANPacker from selfdrive.car.ford import fordcan -from selfdrive.car.ford.values import CarControllerParams +from selfdrive.car.ford.values import CANBUS, CarControllerParams VisualAlert = car.CarControl.HUDControl.VisualAlert @@ -16,9 +16,9 @@ def apply_ford_steer_angle_limits(apply_angle, apply_angle_last, vEgo): apply_angle = clip(apply_angle, (apply_angle_last - max_angle_diff), (apply_angle_last + max_angle_diff)) # absolute limit (LatCtlPath_An_Actl) - apply_path_angle = math.radians(apply_angle) / CarControllerParams.STEER_RATIO - apply_path_angle = clip(apply_path_angle, -0.4995, 0.5240) - apply_angle = math.degrees(apply_path_angle) * CarControllerParams.STEER_RATIO + apply_path_angle = math.radians(apply_angle) / CarControllerParams.LKAS_STEER_RATIO + apply_path_angle = clip(apply_path_angle, -0.5, 0.5235) + apply_angle = math.degrees(apply_path_angle) * CarControllerParams.LKAS_STEER_RATIO return apply_angle @@ -47,40 +47,46 @@ class CarController: ### acc buttons ### if CC.cruiseControl.cancel: can_sends.append(fordcan.create_button_command(self.packer, CS.buttons_stock_values, cancel=True)) - elif CC.cruiseControl.resume: + can_sends.append(fordcan.create_button_command(self.packer, CS.buttons_stock_values, cancel=True, bus=CANBUS.main)) + elif CC.cruiseControl.resume and (self.frame % CarControllerParams.BUTTONS_STEP) == 0: can_sends.append(fordcan.create_button_command(self.packer, CS.buttons_stock_values, resume=True)) - - # if stock lane centering is active or in standby, toggle it off + can_sends.append(fordcan.create_button_command(self.packer, CS.buttons_stock_values, resume=True, bus=CANBUS.main)) + # if stock lane centering isn't off, send a button press to toggle it off # the stock system checks for steering pressed, and eventually disengages cruise control - if (self.frame % 200) == 0 and CS.acc_tja_status_stock_values["Tja_D_Stat"] != 0: + elif CS.acc_tja_status_stock_values["Tja_D_Stat"] != 0 and (self.frame % CarControllerParams.ACC_UI_STEP) == 0: can_sends.append(fordcan.create_button_command(self.packer, CS.buttons_stock_values, tja_toggle=True)) ### lateral control ### if CC.latActive: + lca_rq = 1 apply_angle = apply_ford_steer_angle_limits(actuators.steeringAngleDeg, self.apply_angle_last, CS.out.vEgo) else: - apply_angle = CS.out.steeringAngleDeg + lca_rq = 0 + apply_angle = 0. # send steering commands at 20Hz if (self.frame % CarControllerParams.LKAS_STEER_STEP) == 0: - lca_rq = 1 if CC.latActive else 0 - # use LatCtlPath_An_Actl to actuate steering - # path angle is the car wheel angle, not the steering wheel angle - path_angle = math.radians(apply_angle) / CarControllerParams.STEER_RATIO - - # ramp rate: 0=Slow, 1=Medium, 2=Fast, 3=Immediately - # TODO: try slower ramp speed when driver torque detected - ramp_type = 3 + path_angle = math.radians(apply_angle) / CarControllerParams.LKAS_STEER_RATIO + + # set slower ramp type when small steering angle change + # 0=Slow, 1=Medium, 2=Fast, 3=Immediately + steer_change = abs(CS.out.steeringAngleDeg - actuators.steeringAngleDeg) + if steer_change < 2.0: + ramp_type = 0 + elif steer_change < 4.0: + ramp_type = 1 + elif steer_change < 6.0: + ramp_type = 2 + else: + ramp_type = 3 precision = 1 # 0=Comfortable, 1=Precise (the stock system always uses comfortable) - offset_roll_compensation_curvature = clip(self.VM.calc_curvature(0, CS.out.vEgo, -CS.yaw_data["VehYaw_W_Actl"]), -0.02, 0.02094) - self.apply_angle_last = apply_angle - can_sends.append(fordcan.create_lka_command(self.packer, apply_angle, 0)) + can_sends.append(fordcan.create_lka_command(self.packer, 0, 0)) can_sends.append(fordcan.create_tja_command(self.packer, lca_rq, ramp_type, precision, - 0, path_angle, 0, offset_roll_compensation_curvature)) + 0, path_angle, 0, 0)) ### ui ### @@ -99,7 +105,7 @@ class CarController: self.steer_alert_last = steer_alert new_actuators = actuators.copy() - new_actuators.steeringAngleDeg = apply_angle + new_actuators.steeringAngleDeg = self.apply_angle_last self.frame += 1 return new_actuators, can_sends diff --git a/selfdrive/car/ford/carstate.py b/selfdrive/car/ford/carstate.py index a7ea19effc..2276b1208a 100644 --- a/selfdrive/car/ford/carstate.py +++ b/selfdrive/car/ford/carstate.py @@ -20,7 +20,7 @@ class CarState(CarStateBase): ret = car.CarState.new_message() # car speed - ret.vEgoRaw = cp.vl["EngVehicleSpThrottle2"]["Veh_V_ActlEng"] * CV.KPH_TO_MS + ret.vEgoRaw = cp.vl["BrakeSysFeatures"]["Veh_V_ActlBrk"] * CV.KPH_TO_MS ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw) ret.yawRate = cp.vl["Yaw_Data_FD1"]["VehYaw_W_Actl"] ret.standstill = cp.vl["DesiredTorqBrk"]["VehStop_D_Stat"] == 1 @@ -85,8 +85,6 @@ class CarState(CarStateBase): # Stock values from IPMA so that we can retain some stock functionality self.acc_tja_status_stock_values = cp_cam.vl["ACCDATA_3"] self.lkas_status_stock_values = cp_cam.vl["IPMA_Data"] - # Use stock sensor values - self.yaw_data = cp.vl["Yaw_Data_FD1"] return ret @@ -94,7 +92,7 @@ class CarState(CarStateBase): def get_can_parser(CP): signals = [ # sig_name, sig_address - ("Veh_V_ActlEng", "EngVehicleSpThrottle2"), # ABS vehicle speed (kph) + ("Veh_V_ActlBrk", "BrakeSysFeatures"), # ABS vehicle speed (kph) ("VehYaw_W_Actl", "Yaw_Data_FD1"), # ABS vehicle yaw rate (rad/s) ("VehStop_D_Stat", "DesiredTorqBrk"), # ABS vehicle stopped ("PrkBrkStatus", "DesiredTorqBrk"), # ABS park brake status @@ -156,7 +154,7 @@ class CarState(CarStateBase): checks = [ # sig_address, frequency - ("EngVehicleSpThrottle2", 50), + ("BrakeSysFeatures", 50), ("Yaw_Data_FD1", 100), ("DesiredTorqBrk", 50), ("EngVehicleSpThrottle", 100), diff --git a/selfdrive/car/ford/fordcan.py b/selfdrive/car/ford/fordcan.py index b42561df21..373ce096c6 100644 --- a/selfdrive/car/ford/fordcan.py +++ b/selfdrive/car/ford/fordcan.py @@ -8,8 +8,7 @@ def create_lka_command(packer, angle_deg: float, curvature: float): """ Creates a CAN message for the Ford LKAS Command. - This command can apply "Lane Keeping Aid" manoeuvres, which are subject to the - PSCM lockout. + This command can apply "Lane Keeping Aid" manoeuvres, which are subject to the PSCM lockout. Frequency is 20Hz. """ @@ -30,12 +29,20 @@ def create_tja_command(packer, lca_rq: int, ramp_type: int, precision: int, path """ Creates a CAN message for the Ford TJA/LCA Command. - This command can apply "Lane Centering" manoeuvres: continuous lane centering - for traffic jam assist and highway driving. It is not subject to the PSCM - lockout. + This command can apply "Lane Centering" manoeuvres: continuous lane centering for traffic jam + assist and highway driving. It is not subject to the PSCM lockout. - The PSCM should be configured to accept TJA/LCA commands before these - commands will be processed. This can be done using tools such as Forscan. + Ford lane centering command uses a third order polynomial to describe the road centerline. The + polynomial is defined by the following coefficients: + c0: lateral offset between the vehicle and the centerline + c1: heading angle between the vehicle and the centerline + c2: curvature of the centerline + c3: rate of change of curvature of the centerline + As the PSCM combines this information with other sensor data, such as the vehicle's yaw rate and + speed, the steering angle cannot be easily controlled. + + The PSCM should be configured to accept TJA/LCA commands before these commands will be processed. + This can be done using tools such as Forscan. Frequency is 20Hz. """ @@ -47,7 +54,7 @@ def create_tja_command(packer, lca_rq: int, ramp_type: int, precision: int, path "LatCtlRampType_D_Rq": ramp_type, # Ramp speed: 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3] "LatCtlPrecision_D_Rq": precision, # Precision: 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3] "LatCtlPathOffst_L_Actl": path_offset, # Path offset [-5.12|5.11] meter - "LatCtlPath_An_Actl": path_angle, # Path angle [-0.4995|0.5240] radians + "LatCtlPath_An_Actl": path_angle, # Path angle [-0.5|0.5235] radians "LatCtlCurv_NoRate_Actl": curvature_rate, # Curvature rate [-0.001024|0.00102375] 1/meter^2 "LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter } @@ -108,8 +115,8 @@ def create_lkas_ui_command(packer, main_on: bool, enabled: bool, steer_alert: bo def create_acc_ui_command(packer, main_on: bool, enabled: bool, hud_control, stock_values: dict): """ - Creates a CAN message for the Ford IPC adaptive cruise, forward collision - warning and traffic jam assist status. + Creates a CAN message for the Ford IPC adaptive cruise, forward collision warning and traffic jam + assist status. Stock functionality is maintained by passing through unmodified signals. @@ -141,7 +148,7 @@ def create_acc_ui_command(packer, main_on: bool, enabled: bool, hud_control, sto return packer.make_can_msg("ACCDATA_3", CANBUS.main, values) -def create_button_command(packer, stock_values: dict, cancel = False, resume = False, tja_toggle = False, bus = CANBUS.camera): +def create_button_command(packer, stock_values: dict, cancel = False, resume = False, tja_toggle = False, bus: int = CANBUS.camera): """ Creates a CAN message for the Ford SCCM buttons/switches. diff --git a/selfdrive/car/ford/interface.py b/selfdrive/car/ford/interface.py index 7d4c9eb94c..4943db076f 100644 --- a/selfdrive/car/ford/interface.py +++ b/selfdrive/car/ford/interface.py @@ -5,8 +5,7 @@ from selfdrive.car import STD_CARGO_KG, scale_rot_inertia, scale_tire_stiffness, from selfdrive.car.ford.values import CAR, Ecu, TransmissionType, GearShifter from selfdrive.car.interfaces import CarInterfaceBase - -EventName = car.CarEvent.EventName +CarParams = car.CarParams class CarInterface(CarInterfaceBase): @@ -19,10 +18,10 @@ class CarInterface(CarInterfaceBase): ret.carName = "ford" ret.dashcamOnly = True - ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.ford)] + ret.safetyConfigs = [get_safety_config(CarParams.SafetyModel.ford)] # Angle-based steering - ret.steerControlType = car.CarParams.SteerControlType.angle + ret.steerControlType = CarParams.SteerControlType.angle ret.steerActuatorDelay = 0.4 ret.steerLimitTimer = 1.0 tire_stiffness_factor = 1.0 @@ -43,7 +42,7 @@ class CarInterface(CarInterfaceBase): ret.mass = 1350 + STD_CARGO_KG else: - raise ValueError(f"Unsupported car: ${candidate}") + raise ValueError(f"Unsupported car: {candidate}") # Auto Transmission: 0x732 ECU or Gear_Shift_by_Wire_FD1 found_ecus = [fw.ecu for fw in car_fw] diff --git a/selfdrive/car/ford/values.py b/selfdrive/car/ford/values.py index 5820b5c9fd..7b3140fbbf 100644 --- a/selfdrive/car/ford/values.py +++ b/selfdrive/car/ford/values.py @@ -1,4 +1,4 @@ -from collections import namedtuple +from collections import defaultdict, namedtuple from dataclasses import dataclass from enum import Enum from typing import Dict, List, Union @@ -22,19 +22,17 @@ class CarControllerParams: LKAS_UI_STEP = 100 # Message: ACCDATA_3 ACC_UI_STEP = 5 + # Message: Steering_Data_FD1, but send twice as fast + BUTTONS_STEP = 10 / 2 - STEER_RATIO = 2.75 - STEER_DRIVER_ALLOWANCE = 0.8 + LKAS_STEER_RATIO = 2.75 # Approximate ratio between LatCtlPath_An_Actl and steering angle in radians + # TODO: remove this once we understand how the EPS calculates the steering angle better + STEER_DRIVER_ALLOWANCE = 0.8 # Driver intervention threshold in Nm RATE_LIMIT_UP = AngleRateLimit(speed_points=[0., 5., 15.], max_angle_diff_points=[5., .8, .15]) RATE_LIMIT_DOWN = AngleRateLimit(speed_points=[0., 5., 15.], max_angle_diff_points=[5., 3.5, 0.4]) -class RADAR: - DELPHI_ESR = 'ford_fusion_2018_adas' - DELPHI_MRR = 'FORD_CADS' - - class CANBUS: main = 0 radar = 1 @@ -47,6 +45,14 @@ class CAR: FOCUS_MK4 = "FORD FOCUS 4TH GEN" +class RADAR: + DELPHI_ESR = 'ford_fusion_2018_adas' + DELPHI_MRR = 'FORD_CADS' + + +DBC: Dict[str, Dict[str, str]] = defaultdict(lambda: dbc_dict("ford_lincoln_base_pt", RADAR.DELPHI_MRR)) + + @dataclass class FordCarInfo(CarInfo): package: str = "Co-Pilot360 Assist+" @@ -143,10 +149,3 @@ FW_VERSIONS = { ], }, } - - -DBC = { - CAR.ESCAPE_MK4: dbc_dict('ford_lincoln_base_pt', RADAR.DELPHI_MRR), - CAR.EXPLORER_MK6: dbc_dict('ford_lincoln_base_pt', RADAR.DELPHI_MRR), - CAR.FOCUS_MK4: dbc_dict('ford_lincoln_base_pt', RADAR.DELPHI_MRR), -} diff --git a/selfdrive/car/fw_query_definitions.py b/selfdrive/car/fw_query_definitions.py index c3b74da920..c7e4d4eb30 100755 --- a/selfdrive/car/fw_query_definitions.py +++ b/selfdrive/car/fw_query_definitions.py @@ -2,7 +2,7 @@ import capnp from dataclasses import dataclass, field import struct -from typing import Dict, List +from typing import Dict, List, Optional, Tuple import panda.python.uds as uds @@ -64,3 +64,5 @@ class FwQueryConfig: requests: List[Request] # 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) + # 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) diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 9c0c406f14..d3e8eae0de 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import traceback from collections import defaultdict from typing import Any, Optional, Set, Tuple from tqdm import tqdm @@ -214,6 +213,11 @@ def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, debug=Fa def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, debug=False, progress=False): versions = VERSIONS.copy() + + # Each brand can define extra ECUs to query for data collection + for brand, config in FW_QUERY_CONFIGS.items(): + versions[brand]["debug"] = {ecu: [] for ecu in config.extra_ecus} + if query_brand is not None: versions = {query_brand: versions[query_brand]} @@ -254,23 +258,23 @@ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, if addrs: query = IsoTpParallelQuery(sendcan, logcan, r.bus, addrs, r.request, r.response, r.rx_offset, debug=debug) - for (addr, rx_addr), version in query.get_data(timeout).items(): + for (tx_addr, sub_addr), version in query.get_data(timeout).items(): f = car.CarParams.CarFw.new_message() - f.ecu = ecu_types.get((brand, addr[0], addr[1]), Ecu.unknown) + f.ecu = ecu_types.get((brand, tx_addr, sub_addr), Ecu.unknown) f.fwVersion = version - f.address = addr[0] - f.responseAddress = rx_addr + f.address = tx_addr + f.responseAddress = uds.get_rx_addr_for_tx_addr(tx_addr, r.rx_offset) f.request = r.request f.brand = brand f.bus = r.bus - if addr[1] is not None: - f.subAddress = addr[1] + if sub_addr is not None: + f.subAddress = sub_addr car_fw.append(f) except Exception: - cloudlog.warning(f"FW query exception: {traceback.format_exc()}") + cloudlog.exception("FW query exception") return car_fw @@ -304,8 +308,8 @@ if __name__ == "__main__": t = time.time() print("Getting vin...") - addr, vin_rx_addr, vin = get_vin(logcan, sendcan, 1, retry=10, debug=args.debug) - print(f'TX: {hex(addr)}, RX: {hex(vin_rx_addr)}, VIN: {vin}') + vin_rx_addr, vin = get_vin(logcan, sendcan, 1, retry=10, debug=args.debug) + print(f'RX: {hex(vin_rx_addr)}, VIN: {vin}') print(f"Getting VIN took {time.time() - t:.3f} s") print() diff --git a/selfdrive/car/gm/carcontroller.py b/selfdrive/car/gm/carcontroller.py index 977d20c5b3..7c883dacc0 100644 --- a/selfdrive/car/gm/carcontroller.py +++ b/selfdrive/car/gm/carcontroller.py @@ -21,7 +21,8 @@ class CarController: self.frame = 0 self.last_button_frame = 0 - self.lka_steering_cmd_counter_last = -1 + self.lka_steering_cmd_counter = 0 + self.sent_lka_steering_cmd = False self.lka_icon_status_last = (False, False) self.params = CarControllerParams() @@ -44,9 +45,14 @@ class CarController: # Steering (50Hz) # Avoid GM EPS faults when transmitting messages too close together: skip this transmit if we just received the # next Panda loopback confirmation in the current CS frame. - if CS.lka_steering_cmd_counter != self.lka_steering_cmd_counter_last: - self.lka_steering_cmd_counter_last = CS.lka_steering_cmd_counter + if CS.loopback_lka_steering_cmd_updated: + self.lka_steering_cmd_counter += 1 + self.sent_lka_steering_cmd = True elif (self.frame % self.params.STEER_STEP) == 0: + # Initialize ASCMLKASteeringCmd counter using the camera + if not self.sent_lka_steering_cmd and self.CP.networkLocation == NetworkLocation.fwdCamera: + self.lka_steering_cmd_counter = CS.camera_lka_steering_cmd_counter + 1 + if CC.latActive: new_steer = int(round(actuators.steer * self.params.STEER_MAX)) apply_steer = apply_std_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, self.params) @@ -54,10 +60,7 @@ class CarController: apply_steer = 0 self.apply_steer_last = apply_steer - # GM EPS faults on any gap in received message counters. To handle transient OP/Panda safety sync issues at the - # moment of disengaging, increment the counter based on the last message known to pass Panda safety checks. - idx = (CS.lka_steering_cmd_counter + 1) % 4 - + idx = self.lka_steering_cmd_counter % 4 can_sends.append(gmcan.create_steering_control(self.packer_pt, CanBus.POWERTRAIN, apply_steer, idx, CC.latActive)) if self.CP.openpilotLongitudinalControl: diff --git a/selfdrive/car/gm/carstate.py b/selfdrive/car/gm/carstate.py index 0bba1d29b8..21eb440d7f 100644 --- a/selfdrive/car/gm/carstate.py +++ b/selfdrive/car/gm/carstate.py @@ -15,7 +15,8 @@ class CarState(CarStateBase): super().__init__(CP) can_define = CANDefine(DBC[CP.carFingerprint]["pt"]) self.shifter_values = can_define.dv["ECMPRDNL2"]["PRNDL2"] - self.lka_steering_cmd_counter = 0 + self.loopback_lka_steering_cmd_updated = False + self.camera_lka_steering_cmd_counter = 0 self.buttons_counter = 0 def update(self, pt_cp, cam_cp, loopback_cp): @@ -25,6 +26,11 @@ class CarState(CarStateBase): self.cruise_buttons = pt_cp.vl["ASCMSteeringButton"]["ACCButtons"] self.buttons_counter = pt_cp.vl["ASCMSteeringButton"]["RollingCounter"] + # Variables used for avoiding LKAS faults + self.loopback_lka_steering_cmd_updated = len(loopback_cp.vl_all["ASCMLKASteeringCmd"]) > 0 + if self.CP.networkLocation == NetworkLocation.fwdCamera: + self.camera_lka_steering_cmd_counter = cam_cp.vl["ASCMLKASteeringCmd"]["RollingCounter"] + ret.wheelSpeeds = self.get_wheel_speeds( pt_cp.vl["EBCMWheelSpdFront"]["FLWheelSpd"], pt_cp.vl["EBCMWheelSpdFront"]["FRWheelSpd"], @@ -40,9 +46,17 @@ class CarState(CarStateBase): else: ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(pt_cp.vl["ECMPRDNL2"]["PRNDL2"], None)) - # Brake pedal's potentiometer returns near-zero reading even when pedal is not pressed. - ret.brake = pt_cp.vl["EBCMBrakePedalPosition"]["BrakePedalPosition"] / 0xd0 - ret.brakePressed = pt_cp.vl["EBCMBrakePedalPosition"]["BrakePedalPosition"] >= 10 + # Some Volt 2016-17 have loose brake pedal push rod retainers which causes the ECM to believe + # that the brake is being intermittently pressed without user interaction. + # To avoid a cruise fault we need to match the ECM's brake pressed signal and threshold + # https://static.nhtsa.gov/odi/tsbs/2017/MC-10137629-9999.pdf + ret.brake = pt_cp.vl["ECMAcceleratorPos"]["BrakePedalPos"] + if self.CP.networkLocation != NetworkLocation.fwdCamera: + ret.brakePressed = ret.brake >= 8 + else: + # While car is braking, cancel button causes ECM to enter a soft disable state with a fault status. + # Match ECM threshold at a standstill to allow the camera to cancel earlier + ret.brakePressed = ret.brake >= 20 # Regen braking is braking if self.CP.transmissionType == TransmissionType.direct: @@ -56,7 +70,6 @@ class CarState(CarStateBase): ret.steeringTorque = pt_cp.vl["PSCMStatus"]["LKADriverAppldTrq"] ret.steeringTorqueEps = pt_cp.vl["PSCMStatus"]["LKATorqueDelivered"] ret.steeringPressed = abs(ret.steeringTorque) > STEER_THRESHOLD - self.lka_steering_cmd_counter = loopback_cp.vl["ASCMLKASteeringCmd"]["RollingCounter"] # 0 inactive, 1 active, 2 temporarily limited, 3 failed self.lkas_status = pt_cp.vl["PSCMStatus"]["LKATorqueDeliveredStatus"] @@ -84,6 +97,9 @@ class CarState(CarStateBase): if self.CP.networkLocation == NetworkLocation.fwdCamera: ret.cruiseState.speed = cam_cp.vl["ASCMActiveCruiseControlStatus"]["ACCSpeedSetpoint"] * CV.KPH_TO_MS + ret.stockAeb = cam_cp.vl["AEBCmd"]["AEBCmdActive"] != 0 + ret.stockFcw = cam_cp.vl["ASCMActiveCruiseControlStatus"]["FCWAlert"] != 0 + return ret @staticmethod @@ -91,8 +107,17 @@ class CarState(CarStateBase): signals = [] checks = [] if CP.networkLocation == NetworkLocation.fwdCamera: - signals.append(("ACCSpeedSetpoint", "ASCMActiveCruiseControlStatus")) - checks.append(("ASCMActiveCruiseControlStatus", 25)) + signals += [ + ("AEBCmdActive", "AEBCmd"), + ("RollingCounter", "ASCMLKASteeringCmd"), + ("FCWAlert", "ASCMActiveCruiseControlStatus"), + ("ACCSpeedSetpoint", "ASCMActiveCruiseControlStatus"), + ] + checks += [ + ("AEBCmd", 10), + ("ASCMLKASteeringCmd", 10), + ("ASCMActiveCruiseControlStatus", 25), + ] return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, CanBus.CAMERA) @@ -100,7 +125,7 @@ class CarState(CarStateBase): def get_can_parser(CP): signals = [ # sig_name, sig_address - ("BrakePedalPosition", "EBCMBrakePedalPosition"), + ("BrakePedalPos", "ECMAcceleratorPos"), ("FrontLeftDoor", "BCMDoorBeltStatus"), ("FrontRightDoor", "BCMDoorBeltStatus"), ("RearLeftDoor", "BCMDoorBeltStatus"), @@ -141,7 +166,7 @@ class CarState(CarStateBase): ("ASCMSteeringButton", 33), ("ECMEngineStatus", 100), ("PSCMSteeringAngle", 100), - ("EBCMBrakePedalPosition", 100), + ("ECMAcceleratorPos", 80), ] if CP.transmissionType == TransmissionType.direct: @@ -157,9 +182,7 @@ class CarState(CarStateBase): ] checks = [ - ("ASCMLKASteeringCmd", 10), # 10 Hz is the stock inactive rate (every 100ms). - # While active 50 Hz (every 20 ms) is normal - # EPS will tolerate around 200ms when active before faulting + ("ASCMLKASteeringCmd", 0), ] - return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, CanBus.LOOPBACK) + return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, CanBus.LOOPBACK, enforce_checks=False) diff --git a/selfdrive/car/gm/interface.py b/selfdrive/car/gm/interface.py index 22ea83759a..248828e757 100755 --- a/selfdrive/car/gm/interface.py +++ b/selfdrive/car/gm/interface.py @@ -212,7 +212,13 @@ class CarInterface(CarInterfaceBase): if ret.cruiseState.standstill: events.add(EventName.resumeRequired) if ret.vEgo < self.CP.minSteerSpeed: - events.add(car.CarEvent.EventName.belowSteerSpeed) + events.add(EventName.belowSteerSpeed) + + if self.CP.networkLocation == NetworkLocation.fwdCamera and self.CP.pcmCruise: + # The ECM has a higher brake pressed threshold than the camera, causing an + # ACC fault when you engage at a stop with your foot partially on the brake + if ret.vEgoRaw < 0.1 and ret.brake < 20: + events.add(EventName.gmAccFaultedTemp) ret.events = events.to_msg() diff --git a/selfdrive/car/gm/values.py b/selfdrive/car/gm/values.py index 943e8a6585..999dabfee3 100644 --- a/selfdrive/car/gm/values.py +++ b/selfdrive/car/gm/values.py @@ -23,11 +23,12 @@ class CarControllerParams: ADAS_KEEPALIVE_STEP = 100 CAMERA_KEEPALIVE_STEP = 100 - # Volt gasbrake lookups - # TODO: These values should be confirmed on non-Volt vehicles + # Volt gas/brake lookups + # TODO: These values should be confirmed on non-Volt vehicles. + # MAX_GAS should achieve 2 m/s^2 and MAX_BRAKE with regen should achieve -4.0 m/s^2 MAX_GAS = 3072 # Safety limit, not ACC max. Stock ACC >4096 from standstill. ZERO_GAS = 2048 # Coasting - MAX_BRAKE = 350 # ~ -3.5 m/s^2 with regen + MAX_BRAKE = 400 # ~ -4.0 m/s^2 with regen MAX_ACC_REGEN = 1404 # Max ACC regen is slightly less than max paddle regen # Allow small margin below -3.5 m/s^2 from ISO 15622:2018 since we @@ -38,15 +39,14 @@ class CarControllerParams: ACCEL_MAX = 2. # m/s^2 ACCEL_MIN = -4. # m/s^2 - EV_GAS_LOOKUP_BP = [-1., 0., ACCEL_MAX] - EV_BRAKE_LOOKUP_BP = [ACCEL_MIN, -1.] - # ICE has much less engine braking force compared to regen in EVs, # lower threshold removes some braking deadzone GAS_LOOKUP_BP = [-0.1, 0., ACCEL_MAX] - BRAKE_LOOKUP_BP = [ACCEL_MIN, -0.1] - + EV_GAS_LOOKUP_BP = [-1., 0., ACCEL_MAX] GAS_LOOKUP_V = [MAX_ACC_REGEN, ZERO_GAS, MAX_GAS] + + BRAKE_LOOKUP_BP = [ACCEL_MIN, -0.1] + EV_BRAKE_LOOKUP_BP = [ACCEL_MIN, -1.] BRAKE_LOOKUP_V = [MAX_BRAKE, 0.] @@ -86,13 +86,13 @@ CAR_INFO: Dict[str, Union[GMCarInfo, List[GMCarInfo]]] = { CAR.ACADIA: GMCarInfo("GMC Acadia 2018", video_link="https://www.youtube.com/watch?v=0ZN6DdsBUZo"), CAR.BUICK_REGAL: GMCarInfo("Buick Regal Essence 2018"), CAR.ESCALADE_ESV: GMCarInfo("Cadillac Escalade ESV 2016", "Adaptive Cruise Control (ACC) & LKAS"), - CAR.BOLT_EV: GMCarInfo("Chevrolet Bolt EV 2022-23", "Adaptive Cruise Control (ACC)", footnotes=[], harness=Harness.gm), - CAR.BOLT_EUV: GMCarInfo("Chevrolet Bolt EUV 2022-23", "Premier or Premier Redline Trim without Super Cruise Package", video_link="https://youtu.be/xvwzGMUA210", footnotes=[], harness=Harness.gm), + CAR.BOLT_EV: GMCarInfo("Chevrolet Bolt EV 2022-23", footnotes=[], harness=Harness.gm), + CAR.BOLT_EUV: GMCarInfo("Chevrolet Bolt EUV 2022-23", "Premier or Premier Redline Trim without Super Cruise Package", "https://youtu.be/xvwzGMUA210", footnotes=[], harness=Harness.gm), CAR.SILVERADO: [ GMCarInfo("Chevrolet Silverado 1500 2020-21", "Safety Package II", footnotes=[], harness=Harness.gm), GMCarInfo("GMC Sierra 1500 2020-21", "Driver Alert Package II", footnotes=[], harness=Harness.gm), ], - CAR.EQUINOX: GMCarInfo("Chevrolet Equinox 2019-22", "Adaptive Cruise Control (ACC)", footnotes=[], harness=Harness.gm), + CAR.EQUINOX: GMCarInfo("Chevrolet Equinox 2019-22", footnotes=[], harness=Harness.gm), } diff --git a/selfdrive/car/honda/values.py b/selfdrive/car/honda/values.py index 43c3c77369..e48edc42ba 100644 --- a/selfdrive/car/honda/values.py +++ b/selfdrive/car/honda/values.py @@ -110,13 +110,13 @@ class HondaCarInfo(CarInfo): CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = { 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, harness=Harness.bosch_a), + HondaCarInfo("Honda Accord 2018-22", "All", "https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), HondaCarInfo("Honda Inspire 2018", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), ], CAR.ACCORDH: HondaCarInfo("Honda Accord Hybrid 2018-22", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), CAR.CIVIC: HondaCarInfo("Honda Civic 2016-18", harness=Harness.nidec, video_link="https://youtu.be/-IkImTe1NYE"), CAR.CIVIC_BOSCH: [ - HondaCarInfo("Honda Civic 2019-21", "All", video_link="https://www.youtube.com/watch?v=4Iz1Mz5LGF8", footnotes=[Footnote.CIVIC_DIESEL], min_steer_speed=2. * CV.MPH_TO_MS, harness=Harness.bosch_a), + HondaCarInfo("Honda Civic 2019-21", "All", "https://www.youtube.com/watch?v=4Iz1Mz5LGF8", [Footnote.CIVIC_DIESEL], min_steer_speed=2. * CV.MPH_TO_MS, harness=Harness.bosch_a), HondaCarInfo("Honda Civic Hatchback 2017-21", harness=Harness.bosch_a), ], CAR.CIVIC_BOSCH_DIESEL: None, # same platform diff --git a/selfdrive/car/hyundai/carcontroller.py b/selfdrive/car/hyundai/carcontroller.py index 6f7cc319e4..a5d995df25 100644 --- a/selfdrive/car/hyundai/carcontroller.py +++ b/selfdrive/car/hyundai/carcontroller.py @@ -10,6 +10,12 @@ from selfdrive.car.hyundai.values import HyundaiFlags, Buttons, CarControllerPar VisualAlert = car.CarControl.HUDControl.VisualAlert LongCtrlState = car.CarControl.Actuators.LongControlState +# EPS faults if you apply torque while the steering angle is above 90 degrees for more than 1 second +# All slightly below EPS thresholds to avoid fault +MAX_ANGLE = 85 +MAX_ANGLE_FRAMES = 89 +MAX_ANGLE_CONSECUTIVE_FRAMES = 2 + def process_hud_alert(enabled, fingerprint, hud_control): sys_warning = (hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw)) @@ -40,22 +46,21 @@ class CarController: self.CP = CP self.params = CarControllerParams(CP) self.packer = CANPacker(dbc_name) + self.angle_limit_counter = 0 self.frame = 0 self.apply_steer_last = 0 self.car_fingerprint = CP.carFingerprint self.last_button_frame = 0 - self.accel = 0 def update(self, CC, CS): actuators = CC.actuators hud_control = CC.hudControl - # Steering Torque - - # These cars have significantly more torque than most HKG. Limit to 70% of max. + # steering torque steer = actuators.steer if self.CP.carFingerprint in (CAR.KONA, CAR.KONA_EV, CAR.KONA_HEV, CAR.KONA_EV_2022): + # these cars have significantly more torque than most HKG; limit to 70% of max steer = clip(steer, -0.7, 0.7) new_steer = int(round(steer * self.params.STEER_MAX)) apply_steer = apply_std_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, self.params) @@ -65,52 +70,84 @@ class CarController: self.apply_steer_last = apply_steer + # accel + longitudinal + accel = clip(actuators.accel, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX) + stopping = actuators.longControlState == LongCtrlState.stopping + set_speed_in_units = hud_control.setSpeed * (CV.MS_TO_KPH if CS.is_metric else CV.MS_TO_MPH) + + # HUD messages sys_warning, sys_state, left_lane_warning, right_lane_warning = process_hud_alert(CC.enabled, self.car_fingerprint, hud_control) can_sends = [] + # *** common hyundai stuff *** + + # tester present - w/ no response (keeps relevant ECU disabled) + if self.frame % 100 == 0 and self.CP.openpilotLongitudinalControl: + addr, bus = 0x7d0, 0 + if self.CP.flags & HyundaiFlags.CANFD_HDA2.value: + addr, bus = 0x730, 5 + can_sends.append([addr, 0, b"\x02\x3E\x80\x00\x00\x00\x00\x00", bus]) + + # CAN-FD platforms if self.CP.carFingerprint in CANFD_CAR: + hda2 = self.CP.flags & HyundaiFlags.CANFD_HDA2 + hda2_long = hda2 and self.CP.openpilotLongitudinalControl + # steering control - can_sends.append(hyundaicanfd.create_lkas(self.packer, self.CP, CC.enabled, CC.latActive, apply_steer)) + can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, CC.enabled, CC.latActive, apply_steer)) - # block LFA on HDA2 - if self.frame % 5 == 0 and (self.CP.flags & HyundaiFlags.CANFD_HDA2): + # disable LFA on HDA2 + if self.frame % 5 == 0 and hda2: can_sends.append(hyundaicanfd.create_cam_0x2a4(self.packer, CS.cam_0x2a4)) # LFA and HDA icons - if self.frame % 2 == 0 and not (self.CP.flags & HyundaiFlags.CANFD_HDA2): - can_sends.append(hyundaicanfd.create_lfahda_cluster(self.packer, CC.enabled)) - - # button presses - if (self.frame - self.last_button_frame) * DT_CTRL > 0.25: - # cruise cancel - if CC.cruiseControl.cancel: - if self.CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS: - can_sends.append(hyundaicanfd.create_cruise_info(self.packer, CS.cruise_info_copy, True)) - self.last_button_frame = self.frame - else: - for _ in range(20): - can_sends.append(hyundaicanfd.create_buttons(self.packer, CS.buttons_counter+1, Buttons.CANCEL)) - self.last_button_frame = self.frame + if self.frame % 5 == 0 and (not hda2 or hda2_long): + can_sends.append(hyundaicanfd.create_lfahda_cluster(self.packer, self.CP, CC.enabled)) - # cruise standstill resume - elif CC.cruiseControl.resume: - if not (self.CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS): - for _ in range(20): - can_sends.append(hyundaicanfd.create_buttons(self.packer, CS.buttons_counter+1, Buttons.RES_ACCEL)) - self.last_button_frame = self.frame + if self.CP.openpilotLongitudinalControl: + can_sends.extend(hyundaicanfd.create_adrv_messages(self.packer, self.frame)) + if self.frame % 2 == 0: + can_sends.append(hyundaicanfd.create_acc_control(self.packer, self.CP, CC.enabled, accel, stopping, CC.cruiseControl.override, + set_speed_in_units)) + else: + # button presses + if (self.frame - self.last_button_frame) * DT_CTRL > 0.25: + # cruise cancel + if CC.cruiseControl.cancel: + if self.CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS: + can_sends.append(hyundaicanfd.create_acc_cancel(self.packer, self.CP, CS.cruise_info)) + self.last_button_frame = self.frame + else: + for _ in range(20): + can_sends.append(hyundaicanfd.create_buttons(self.packer, CS.buttons_counter+1, Buttons.CANCEL)) + self.last_button_frame = self.frame + + # cruise standstill resume + elif CC.cruiseControl.resume: + if not (self.CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS): + for _ in range(20): + can_sends.append(hyundaicanfd.create_buttons(self.packer, CS.buttons_counter+1, Buttons.RES_ACCEL)) + self.last_button_frame = self.frame else: + # Count up to MAX_ANGLE_FRAMES, at which point we need to cut torque to avoid a steering fault + if CC.latActive and abs(CS.out.steeringAngleDeg) >= MAX_ANGLE: + self.angle_limit_counter += 1 + else: + self.angle_limit_counter = 0 - # tester present - w/ no response (keeps radar disabled) - if self.CP.openpilotLongitudinalControl: - if self.frame % 100 == 0: - can_sends.append([0x7D0, 0, b"\x02\x3E\x80\x00\x00\x00\x00\x00", 0]) + # Cut steer actuation bit for two frames and hold torque with induced temporary fault + torque_fault = CC.latActive and self.angle_limit_counter > MAX_ANGLE_FRAMES + lat_active = CC.latActive and not torque_fault - can_sends.append(hyundaican.create_lkas11(self.packer, self.frame, self.car_fingerprint, apply_steer, CC.latActive, - CS.lkas11, sys_warning, sys_state, CC.enabled, - hud_control.leftLaneVisible, hud_control.rightLaneVisible, - left_lane_warning, right_lane_warning)) + if self.angle_limit_counter >= MAX_ANGLE_FRAMES + MAX_ANGLE_CONSECUTIVE_FRAMES: + self.angle_limit_counter = 0 + + can_sends.append(hyundaican.create_lkas11(self.packer, self.frame, self.car_fingerprint, apply_steer, lat_active, + torque_fault, CS.lkas11, sys_warning, sys_state, CC.enabled, + hud_control.leftLaneVisible, hud_control.rightLaneVisible, + left_lane_warning, right_lane_warning)) if not self.CP.openpilotLongitudinalControl: if CC.cruiseControl.cancel: @@ -123,18 +160,10 @@ class CarController: self.last_button_frame = self.frame if self.frame % 2 == 0 and self.CP.openpilotLongitudinalControl: - accel = actuators.accel - - #TODO unclear if this is needed + # TODO: unclear if this is needed jerk = 3.0 if actuators.longControlState == LongCtrlState.pid else 1.0 - - accel = clip(accel, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX) - - stopping = actuators.longControlState == LongCtrlState.stopping - set_speed_in_units = hud_control.setSpeed * (CV.MS_TO_MPH if CS.clu11["CF_Clu_SPEED_UNIT"] == 1 else CV.MS_TO_KPH) can_sends.extend(hyundaican.create_acc_commands(self.packer, CC.enabled, accel, jerk, int(self.frame / 2), - hud_control.leadVisible, set_speed_in_units, stopping, CS.out.gasPressed)) - self.accel = accel + hud_control.leadVisible, set_speed_in_units, stopping, CC.cruiseControl.override)) # 20 Hz LFA MFA message if self.frame % 5 == 0 and self.car_fingerprint in (CAR.SONATA, CAR.PALISADE, CAR.IONIQ, CAR.KIA_NIRO_EV, CAR.KIA_NIRO_HEV_2021, @@ -153,7 +182,7 @@ class CarController: new_actuators = actuators.copy() new_actuators.steer = apply_steer / self.params.STEER_MAX - new_actuators.accel = self.accel + new_actuators.accel = accel self.frame += 1 return new_actuators, can_sends diff --git a/selfdrive/car/hyundai/carstate.py b/selfdrive/car/hyundai/carstate.py index 61da04d04b..5da1dd72c8 100644 --- a/selfdrive/car/hyundai/carstate.py +++ b/selfdrive/car/hyundai/carstate.py @@ -30,10 +30,12 @@ class CarState(CarStateBase): else: # preferred and elect gear methods use same definition self.shifter_values = can_define.dv["LVR12"]["CF_Lvr_Gear"] + self.is_metric = False self.brake_error = False - self.park_brake = False self.buttons_counter = 0 + self.cruise_info = {} + # On some cars, CLU15->CF_Clu_VehicleSpeed can oscillate faster than the dash updates. Sample at 5 Hz self.cluster_speed = 0 self.cluster_speed_counter = CLUSTER_SAMPLE_RATE @@ -46,8 +48,8 @@ class CarState(CarStateBase): ret = car.CarState.new_message() cp_cruise = cp_cam if self.CP.carFingerprint in CAMERA_SCC_CAR else cp - is_metric = cp.vl["CLU11"]["CF_Clu_SPEED_UNIT"] == 0 - speed_conv = CV.KPH_TO_MS if is_metric else CV.MPH_TO_MS + self.is_metric = cp.vl["CLU11"]["CF_Clu_SPEED_UNIT"] == 0 + speed_conv = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS ret.doorOpen = any([cp.vl["CGW1"]["CF_Gway_DrvDrSw"], cp.vl["CGW1"]["CF_Gway_AstDrSw"], cp.vl["CGW2"]["CF_Gway_RLDrSw"], cp.vl["CGW2"]["CF_Gway_RRDrSw"]]) @@ -70,7 +72,7 @@ class CarState(CarStateBase): self.cluster_speed_counter = 0 # mimic how dash converts to imperial - if not is_metric: + if not self.is_metric: self.cluster_speed = math.floor(self.cluster_speed * CV.KPH_TO_MPH + CV.KPH_TO_MPH) ret.vEgoCluster = self.cluster_speed * speed_conv @@ -127,12 +129,12 @@ class CarState(CarStateBase): ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(gear)) if not self.CP.openpilotLongitudinalControl: - if self.CP.carFingerprint in FEATURES["use_fca"]: - ret.stockAeb = cp_cruise.vl["FCA11"]["FCA_CmdAct"] != 0 - ret.stockFcw = cp_cruise.vl["FCA11"]["CF_VSM_Warn"] == 2 - else: - ret.stockAeb = cp_cruise.vl["SCC12"]["AEB_CmdAct"] != 0 - ret.stockFcw = cp_cruise.vl["SCC12"]["CF_VSM_Warn"] == 2 + aeb_src = "FCA11" if self.CP.carFingerprint in FEATURES["use_fca"] else "SCC12" + aeb_sig = "FCA_CmdAct" if self.CP.carFingerprint in FEATURES["use_fca"] else "AEB_CmdAct" + aeb_warning = cp_cruise.vl[aeb_src]["CF_VSM_Warn"] != 0 + aeb_braking = cp_cruise.vl[aeb_src]["CF_VSM_DecCmdAct"] != 0 or cp_cruise.vl[aeb_src][aeb_sig] != 0 + ret.stockFcw = aeb_warning and not aeb_braking + ret.stockAeb = aeb_warning and aeb_braking if self.CP.enableBsm: ret.leftBlindspot = cp.vl["LCA11"]["CF_Lca_IndLeft"] != 0 @@ -152,9 +154,9 @@ class CarState(CarStateBase): def update_canfd(self, cp, cp_cam): ret = car.CarState.new_message() - if self.CP.flags & HyundaiFlags.CANFD_HDA2: + if self.CP.carFingerprint in EV_CAR: ret.gas = cp.vl["ACCELERATOR"]["ACCELERATOR_PEDAL"] / 255. - else: + elif self.CP.carFingerprint in HYBRID_CAR: ret.gas = cp.vl["ACCELERATOR_ALT"]["ACCELERATOR_PEDAL"] / 1023. ret.gasPressed = ret.gas > 1e-5 ret.brakePressed = cp.vl["BRAKE"]["BRAKE_PRESSED"] == 1 @@ -181,22 +183,26 @@ class CarState(CarStateBase): ret.steeringTorque = cp.vl["MDPS"]["STEERING_COL_TORQUE"] ret.steeringTorqueEps = cp.vl["MDPS"]["STEERING_OUT_TORQUE"] ret.steeringPressed = abs(ret.steeringTorque) > self.params.STEER_THRESHOLD + ret.steerFaultTemporary = cp.vl["MDPS"]["LKA_FAULT"] != 0 ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_lamp(50, cp.vl["BLINKERS"]["LEFT_LAMP"], cp.vl["BLINKERS"]["RIGHT_LAMP"]) ret.cruiseState.available = True ret.cruiseState.enabled = cp.vl["SCC1"]["CRUISE_ACTIVE"] == 1 - cp_cruise_info = cp if self.CP.flags & HyundaiFlags.CANFD_HDA2 else cp_cam - speed_factor = CV.MPH_TO_MS if cp.vl["CLUSTER_INFO"]["DISTANCE_UNIT"] == 1 else CV.KPH_TO_MS - ret.cruiseState.speed = cp_cruise_info.vl["CRUISE_INFO"]["SET_SPEED"] * speed_factor - ret.cruiseState.standstill = cp_cruise_info.vl["CRUISE_INFO"]["CRUISE_STANDSTILL"] == 1 + self.is_metric = cp.vl["CLUSTER_INFO"]["DISTANCE_UNIT"] != 1 + if not self.CP.openpilotLongitudinalControl: + speed_factor = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS + cp_cruise_info = cp if self.CP.flags & HyundaiFlags.CANFD_HDA2 else cp_cam + ret.cruiseState.speed = cp_cruise_info.vl["CRUISE_INFO"]["SET_SPEED"] * speed_factor + ret.cruiseState.standstill = cp_cruise_info.vl["CRUISE_INFO"]["CRUISE_STANDSTILL"] == 1 + self.cruise_info = copy.copy(cp_cruise_info.vl["CRUISE_INFO"]) cruise_btn_msg = "CRUISE_BUTTONS_ALT" if self.CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS else "CRUISE_BUTTONS" + self.prev_cruise_buttons = self.cruise_buttons[-1] self.cruise_buttons.extend(cp.vl_all[cruise_btn_msg]["CRUISE_BUTTONS"]) self.main_buttons.extend(cp.vl_all[cruise_btn_msg]["ADAPTIVE_CRUISE_MAIN_BTN"]) self.buttons_counter = cp.vl[cruise_btn_msg]["COUNTER"] - self.cruise_info_copy = copy.copy(cp_cruise_info.vl["CRUISE_INFO"]) if self.CP.flags & HyundaiFlags.CANFD_HDA2: self.cam_0x2a4 = copy.copy(cp_cam.vl["CAM_0x2a4"]) @@ -295,12 +301,14 @@ class CarState(CarStateBase): signals += [ ("FCA_CmdAct", "FCA11"), ("CF_VSM_Warn", "FCA11"), + ("CF_VSM_DecCmdAct", "FCA11"), ] checks.append(("FCA11", 50)) else: signals += [ ("AEB_CmdAct", "SCC12"), ("CF_VSM_Warn", "SCC12"), + ("CF_VSM_DecCmdAct", "SCC12"), ] if CP.enableBsm: @@ -384,12 +392,14 @@ class CarState(CarStateBase): signals += [ ("FCA_CmdAct", "FCA11"), ("CF_VSM_Warn", "FCA11"), + ("CF_VSM_DecCmdAct", "FCA11"), ] checks.append(("FCA11", 50)) else: signals += [ ("AEB_CmdAct", "SCC12"), ("CF_VSM_Warn", "SCC12"), + ("CF_VSM_DecCmdAct", "SCC12"), ] return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, 2) @@ -411,6 +421,7 @@ class CarState(CarStateBase): ("STEERING_ANGLE", "STEERING_SENSORS"), ("STEERING_COL_TORQUE", "MDPS"), ("STEERING_OUT_TORQUE", "MDPS"), + ("LKA_FAULT", "MDPS"), ("CRUISE_ACTIVE", "SCC1"), ("COUNTER", cruise_btn_msg), @@ -439,18 +450,23 @@ class CarState(CarStateBase): ("DOORS_SEATBELTS", 4), ] - if CP.flags & HyundaiFlags.CANFD_HDA2: + if CP.flags & HyundaiFlags.CANFD_HDA2 and not CP.openpilotLongitudinalControl: signals += [ - ("ACCELERATOR_PEDAL", "ACCELERATOR"), - ("GEAR", "ACCELERATOR"), ("SET_SPEED", "CRUISE_INFO"), ("CRUISE_STANDSTILL", "CRUISE_INFO"), ] checks += [ ("CRUISE_INFO", 50), + ] + + if CP.carFingerprint in EV_CAR: + signals += [ + ("ACCELERATOR_PEDAL", "ACCELERATOR"), + ] + checks += [ ("ACCELERATOR", 100), ] - else: + elif CP.carFingerprint in HYBRID_CAR: signals += [ ("ACCELERATOR_PEDAL", "ACCELERATOR_ALT"), ] @@ -473,17 +489,14 @@ class CarState(CarStateBase): ("CRUISE_MAIN", "CRUISE_INFO"), ("CRUISE_STATUS", "CRUISE_INFO"), ("CRUISE_INACTIVE", "CRUISE_INFO"), - ("NEW_SIGNAL_2", "CRUISE_INFO"), + ("ZEROS_9", "CRUISE_INFO"), ("CRUISE_STANDSTILL", "CRUISE_INFO"), - ("NEW_SIGNAL_3", "CRUISE_INFO"), - ("BYTE11", "CRUISE_INFO"), + ("ZEROS_5", "CRUISE_INFO"), + ("DISTANCE_SETTING", "CRUISE_INFO"), ("SET_SPEED", "CRUISE_INFO"), ("NEW_SIGNAL_4", "CRUISE_INFO"), ] - signals += [(f"BYTE{i}", "CRUISE_INFO") for i in range(3, 7)] - signals += [(f"BYTE{i}", "CRUISE_INFO") for i in range(13, 31)] - checks = [ ("CRUISE_INFO", 50), ] diff --git a/selfdrive/car/hyundai/hyundaican.py b/selfdrive/car/hyundai/hyundaican.py index b3e1aa6b66..dcb8430976 100644 --- a/selfdrive/car/hyundai/hyundaican.py +++ b/selfdrive/car/hyundai/hyundaican.py @@ -4,7 +4,7 @@ from selfdrive.car.hyundai.values import CAR, CHECKSUM, CAMERA_SCC_CAR hyundai_checksum = crcmod.mkCrcFun(0x11D, initCrc=0xFD, rev=False, xorOut=0xdf) def create_lkas11(packer, frame, car_fingerprint, apply_steer, steer_req, - lkas11, sys_warning, sys_state, enabled, + torque_fault, lkas11, sys_warning, sys_state, enabled, left_lane, right_lane, left_lane_depart, right_lane_depart): values = lkas11 @@ -14,6 +14,7 @@ def create_lkas11(packer, frame, car_fingerprint, apply_steer, steer_req, values["CF_Lkas_LdwsRHWarning"] = right_lane_depart values["CR_Lkas_StrToqReq"] = apply_steer values["CF_Lkas_ActToi"] = steer_req + values["CF_Lkas_ToiFlt"] = torque_fault # seems to allow actuation on CR_Lkas_StrToqReq values["CF_Lkas_MsgCount"] = frame % 0x10 if car_fingerprint in (CAR.SONATA, CAR.PALISADE, CAR.KIA_NIRO_EV, CAR.KIA_NIRO_HEV_2021, CAR.SANTA_FE, @@ -38,7 +39,7 @@ def create_lkas11(packer, frame, car_fingerprint, apply_steer, steer_req, values["CF_Lkas_SysWarning"] = 4 if sys_warning else 0 # Likely cars lacking the ability to show individual lane lines in the dash - elif car_fingerprint in (CAR.KIA_OPTIMA, CAR.KIA_OPTIMA_2019): + elif car_fingerprint in (CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL): # SysWarning 4 = keep hands on wheel + beep values["CF_Lkas_SysWarning"] = 4 if sys_warning else 0 @@ -94,7 +95,7 @@ def create_lfahda_mfc(packer, enabled, hda_set_speed=0): } return packer.make_can_msg("LFAHDA_MFC", 0, values) -def create_acc_commands(packer, enabled, accel, upper_jerk, idx, lead_visible, set_speed, stopping, gas_pressed): +def create_acc_commands(packer, enabled, accel, upper_jerk, idx, lead_visible, set_speed, stopping, long_override): commands = [] scc11_values = { @@ -111,7 +112,7 @@ def create_acc_commands(packer, enabled, accel, upper_jerk, idx, lead_visible, s commands.append(packer.make_can_msg("SCC11", 0, scc11_values)) scc12_values = { - "ACCMode": 2 if enabled and gas_pressed else 1 if enabled else 0, + "ACCMode": 2 if enabled and long_override else 1 if enabled else 0, "StopReq": 1 if stopping else 0, "aReqRaw": accel, "aReqValue": accel, # stock ramps up and down respecting jerk limit until it reaches aReqRaw @@ -127,23 +128,21 @@ def create_acc_commands(packer, enabled, accel, upper_jerk, idx, lead_visible, s "ComfortBandLower": 0.0, # stock usually is 0 but sometimes uses higher values "JerkUpperLimit": upper_jerk, # stock usually is 1.0 but sometimes uses higher values "JerkLowerLimit": 5.0, # stock usually is 0.5 but sometimes uses higher values - "ACCMode": 2 if enabled and gas_pressed else 1 if enabled else 4, # stock will always be 4 instead of 0 after first disengage + "ACCMode": 2 if enabled and long_override else 1 if enabled else 4, # stock will always be 4 instead of 0 after first disengage "ObjGap": 2 if lead_visible else 0, # 5: >30, m, 4: 25-30 m, 3: 20-25 m, 2: < 20 m, 0: no lead } commands.append(packer.make_can_msg("SCC14", 0, scc14_values)) + # note that some vehicles most likely have an alternate checksum/counter definition + # https://github.com/commaai/opendbc/commit/9ddcdb22c4929baf310295e832668e6e7fcfa602 fca11_values = { - # seems to count 2,1,0,3,2,1,0,3,2,1,0,3,2,1,0,repeat... - # (where first value is aligned to Supplemental_Counter == 0) - # test: [(idx % 0xF, -((idx % 0xF) + 2) % 4) for idx in range(0x14)] - "CR_FCA_Alive": ((-((idx % 0xF) + 2) % 4) << 2) + 1, - "Supplemental_Counter": idx % 0xF, + "CR_FCA_Alive": idx % 0xF, "PAINT1_Status": 1, "FCA_DrvSetStatus": 1, "FCA_Status": 1, # AEB disabled } fca11_dat = packer.make_can_msg("FCA11", 0, fca11_values)[2] - fca11_values["CR_FCA_ChkSum"] = 0x10 - sum(sum(divmod(i, 16)) for i in fca11_dat) % 0x10 + fca11_values["CR_FCA_ChkSum"] = hyundai_checksum(fca11_dat[:7]) commands.append(packer.make_can_msg("FCA11", 0, fca11_values)) return commands diff --git a/selfdrive/car/hyundai/hyundaicanfd.py b/selfdrive/car/hyundai/hyundaicanfd.py index a53be7627d..f2cbafdcf0 100644 --- a/selfdrive/car/hyundai/hyundaicanfd.py +++ b/selfdrive/car/hyundai/hyundaicanfd.py @@ -1,7 +1,17 @@ from selfdrive.car.hyundai.values import HyundaiFlags -def create_lkas(packer, CP, enabled, lat_active, apply_steer): +def get_e_can_bus(CP): + # On the CAN-FD platforms, the LKAS camera is on both A-CAN and E-CAN. HDA2 cars + # have a a different harness than the HDA1 and non-HDA variants in order to split + # a different bus, since the steering is done by different ECUs. + return 5 if CP.flags & HyundaiFlags.CANFD_HDA2 else 4 + + +def create_steering_messages(packer, CP, enabled, lat_active, apply_steer): + + ret = [] + values = { "LKA_MODE": 2, "LKA_ICON": 2 if enabled else 1, @@ -14,8 +24,14 @@ def create_lkas(packer, CP, enabled, lat_active, apply_steer): "NEW_SIGNAL_2": 0, } - msg = "LKAS" if CP.flags & HyundaiFlags.CANFD_HDA2 else "LFA" - return packer.make_can_msg(msg, 4, values) + if CP.flags & HyundaiFlags.CANFD_HDA2: + if CP.openpilotLongitudinalControl: + ret.append(packer.make_can_msg("LFA", 5, values)) + ret.append(packer.make_can_msg("LKAS", 4, values)) + else: + ret.append(packer.make_can_msg("LFA", 4, values)) + + return ret def create_cam_0x2a4(packer, camera_values): camera_values.update({ @@ -31,16 +47,98 @@ def create_buttons(packer, cnt, btn): } return packer.make_can_msg("CRUISE_BUTTONS", 5, values) -def create_cruise_info(packer, cruise_info_copy, cancel): +def create_acc_cancel(packer, CP, cruise_info_copy): values = cruise_info_copy - if cancel: - values["CRUISE_STATUS"] = 0 - values["CRUISE_INACTIVE"] = 1 - return packer.make_can_msg("CRUISE_INFO", 4, values) + values.update({ + "CRUISE_STATUS": 0, + "CRUISE_INACTIVE": 1, + }) + return packer.make_can_msg("CRUISE_INFO", get_e_can_bus(CP), values) -def create_lfahda_cluster(packer, enabled): +def create_lfahda_cluster(packer, CP, enabled): values = { "HDA_ICON": 1 if enabled else 0, "LFA_ICON": 2 if enabled else 0, } - return packer.make_can_msg("LFAHDA_CLUSTER", 4, values) + return packer.make_can_msg("LFAHDA_CLUSTER", get_e_can_bus(CP), values) + + +def create_acc_control(packer, CP, enabled, accel, stopping, gas_override, set_speed): + cruise_status = 0 if not enabled else (4 if gas_override else 2) + if not enabled or gas_override: + accel = 0 + values = { + "CRUISE_STATUS": cruise_status, + "CRUISE_INACTIVE": 0 if enabled else 1, + "CRUISE_MAIN": 1, + "CRUISE_STANDSTILL": 0, + "STOP_REQ": 1 if stopping else 0, + "ACCEL_REQ": accel, + "ACCEL_REQ2": accel, + "SET_SPEED": set_speed, + "DISTANCE_SETTING": 4, + + "ACC_ObjDist": 1, + "ObjValid": 1, + "OBJ_STATUS": 2, + "SET_ME_2": 0x2, + "SET_ME_3": 0x3, + "SET_ME_TMP_64": 0x64, + + "NEW_SIGNAL_9": 2, + "NEW_SIGNAL_10": 4, + } + + return packer.make_can_msg("CRUISE_INFO", get_e_can_bus(CP), values) + + + +def create_adrv_messages(packer, frame): + # messages needed to car happy after disabling + # the ADAS Driving ECU to do longitudinal control + + ret = [] + + values = { + } + ret.append(packer.make_can_msg("ADRV_0x51", 4, values)) + + if frame % 2 == 0: + values = { + 'AEB_SETTING': 0x1, # show AEB disabled icon + 'SET_ME_2': 0x2, + 'SET_ME_FF': 0xff, + 'SET_ME_FC': 0xfc, + 'SET_ME_9': 0x9, + } + ret.append(packer.make_can_msg("ADRV_0x160", 5, values)) + + if frame % 5 == 0: + values = { + 'SET_ME_1C': 0x1c, + 'SET_ME_FF': 0xff, + 'SET_ME_TMP_F': 0xf, + 'SET_ME_TMP_F_2': 0xf, + } + ret.append(packer.make_can_msg("ADRV_0x1ea", 5, values)) + + values = { + 'SET_ME_E1': 0xe1, + 'SET_ME_3A': 0x3a, + } + ret.append(packer.make_can_msg("ADRV_0x200", 5, values)) + + if frame % 20 == 0: + values = { + 'SET_ME_15': 0x15, + } + ret.append(packer.make_can_msg("ADRV_0x345", 5, values)) + + if frame % 100 == 0: + values = { + 'SET_ME_22': 0x22, + 'SET_ME_41': 0x41, + } + ret.append(packer.make_can_msg("ADRV_0x1da", 5, values)) + + return ret diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index 97c1f7a5dc..13e93c7d06 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -27,31 +27,24 @@ class CarInterface(CarInterfaceBase): ret.carName = "hyundai" ret.radarOffCan = RADAR_START_ADDR not in fingerprint[1] or DBC[ret.carFingerprint]["radar"] is None - # WARNING: disabling radar also disables AEB (and we show the same warning on the instrument cluster as if you manually disabled AEB) - ret.experimentalLongitudinalAvailable = candidate not in (LEGACY_SAFETY_MODE_CAR | CAMERA_SCC_CAR | CANFD_CAR) - ret.openpilotLongitudinalControl = experimental_long and ret.experimentalLongitudinalAvailable - - ret.pcmCruise = not ret.openpilotLongitudinalControl - # 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.KIA_OPTIMA_H, CAR.ELANTRA_GT_I30} + if candidate in CANFD_CAR: + # detect HDA2 with LKAS message + if 0x50 in fingerprint[6]: + ret.flags |= HyundaiFlags.CANFD_HDA2.value + else: + # non-HDA2 + if 0x1cf not in fingerprint[4]: + ret.flags |= HyundaiFlags.CANFD_ALT_BUTTONS.value + ret.steerActuatorDelay = 0.1 # Default delay ret.steerLimitTimer = 0.4 tire_stiffness_factor = 1. - ret.stoppingControl = True - ret.startingState = True - ret.vEgoStarting = 0.1 - ret.startAccel = 2.0 - - ret.longitudinalTuning.kpV = [0.5] - ret.longitudinalTuning.kiV = [0.0] - - ret.longitudinalActuatorDelayLowerBound = 0.5 # s - ret.longitudinalActuatorDelayUpperBound = 0.5 # s if candidate in (CAR.SANTA_FE, CAR.SANTA_FE_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022): ret.lateralTuning.pid.kf = 0.00005 ret.mass = 3982. * CV.LB_TO_KG + STD_CARGO_KG @@ -202,12 +195,12 @@ class CarInterface(CarInterfaceBase): ret.lateralTuning.indi.timeConstantV = [1.4] ret.lateralTuning.indi.actuatorEffectivenessBP = [0.] ret.lateralTuning.indi.actuatorEffectivenessV = [1.8] - elif candidate in (CAR.KIA_OPTIMA, CAR.KIA_OPTIMA_2019, CAR.KIA_OPTIMA_H): + elif candidate in (CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, CAR.KIA_OPTIMA_H): ret.mass = 3558. * CV.LB_TO_KG ret.wheelbase = 2.80 ret.steerRatio = 13.75 tire_stiffness_factor = 0.5 - if candidate == CAR.KIA_OPTIMA: + if candidate == CAR.KIA_OPTIMA_G4: ret.minSteerSpeed = 32 * CV.MPH_TO_MS CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) elif candidate == CAR.KIA_STINGER: @@ -291,20 +284,36 @@ class CarInterface(CarInterfaceBase): ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.16], [0.01]] - # panda safety config + # *** longitudinal control *** + if candidate in CANFD_CAR: + ret.longitudinalTuning.kpV = [0.1] + ret.longitudinalTuning.kiV = [0.0] + ret.longitudinalActuatorDelayLowerBound = 0.15 + ret.longitudinalActuatorDelayUpperBound = 0.5 + ret.experimentalLongitudinalAvailable = bool(ret.flags & HyundaiFlags.CANFD_HDA2) + else: + ret.longitudinalTuning.kpV = [0.5] + ret.longitudinalTuning.kiV = [0.0] + ret.longitudinalActuatorDelayLowerBound = 0.5 + ret.longitudinalActuatorDelayUpperBound = 0.5 + ret.experimentalLongitudinalAvailable = candidate not in (LEGACY_SAFETY_MODE_CAR | CAMERA_SCC_CAR) + ret.openpilotLongitudinalControl = experimental_long and ret.experimentalLongitudinalAvailable + ret.pcmCruise = not ret.openpilotLongitudinalControl + + ret.stoppingControl = True + ret.startingState = True + ret.vEgoStarting = 0.1 + ret.startAccel = 2.0 + + # *** panda safety config *** if candidate in CANFD_CAR: ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.noOutput), get_safety_config(car.CarParams.SafetyModel.hyundaiCanfd)] - # detect HDA2 with LKAS message - if 0x50 in fingerprint[6]: - ret.flags |= HyundaiFlags.CANFD_HDA2.value + if ret.flags & HyundaiFlags.CANFD_HDA2: ret.safetyConfigs[1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_HDA2 - else: - # non-HDA2 - if 0x1cf not in fingerprint[4]: - ret.flags |= HyundaiFlags.CANFD_ALT_BUTTONS.value - ret.safetyConfigs[1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_ALT_BUTTONS + if ret.flags & HyundaiFlags.CANFD_ALT_BUTTONS: + ret.safetyConfigs[1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_ALT_BUTTONS else: ret.enableBsm = 0x58b in fingerprint[0] @@ -314,18 +323,16 @@ class CarInterface(CarInterfaceBase): else: ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.hyundai, 0)] - # set appropriate safety param for gas signal - if candidate in HYBRID_CAR: - ret.safetyConfigs[0].safetyParam = 2 - elif candidate in EV_CAR: - ret.safetyConfigs[0].safetyParam = 1 - - if ret.openpilotLongitudinalControl: - ret.safetyConfigs[0].safetyParam |= Panda.FLAG_HYUNDAI_LONG - if candidate in CAMERA_SCC_CAR: ret.safetyConfigs[0].safetyParam |= Panda.FLAG_HYUNDAI_CAMERA_SCC + if ret.openpilotLongitudinalControl: + ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_LONG + if candidate in HYBRID_CAR: + ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_HYBRID_GAS + elif candidate in EV_CAR: + ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_EV_GAS + ret.centerToFront = ret.wheelbase * 0.4 # TODO: get actual value, for now starting with reasonable value for @@ -336,13 +343,15 @@ class CarInterface(CarInterfaceBase): # mass and CG position, so all cars will have approximately similar dyn behaviors ret.tireStiffnessFront, ret.tireStiffnessRear = scale_tire_stiffness(ret.mass, ret.wheelbase, ret.centerToFront, tire_stiffness_factor=tire_stiffness_factor) - return ret @staticmethod def init(CP, logcan, sendcan): if CP.openpilotLongitudinalControl: - disable_ecu(logcan, sendcan, addr=0x7d0, com_cont_req=b'\x28\x83\x01') + addr, bus = 0x7d0, 0 + if CP.flags & HyundaiFlags.CANFD_HDA2.value: + addr, bus = 0x730, 5 + disable_ecu(logcan, sendcan, bus=bus, addr=addr, com_cont_req=b'\x28\x83\x01') def _update(self, c): ret = self.CS.update(self.cp, self.cp_cam) diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index f0e78b22b8..734e56c033 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -85,8 +85,8 @@ class CAR: KIA_NIRO_EV = "KIA NIRO EV 2020" KIA_NIRO_PHEV = "KIA NIRO HYBRID 2019" KIA_NIRO_HEV_2021 = "KIA NIRO HYBRID 2021" - KIA_OPTIMA = "KIA OPTIMA 2016" - KIA_OPTIMA_2019 = "KIA OPTIMA 2019" + KIA_OPTIMA_G4 = "KIA OPTIMA 4TH GEN" + KIA_OPTIMA_G4_FL = "KIA OPTIMA 4TH GEN FACELIFT" KIA_OPTIMA_H = "KIA OPTIMA HYBRID 2017 & SPORTS 2019" KIA_SELTOS = "KIA SELTOS 2021" KIA_SORENTO = "KIA SORENTO GT LINE 2018" @@ -109,11 +109,14 @@ class HyundaiCarInfo(CarInfo): CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = { CAR.ELANTRA: HyundaiCarInfo("Hyundai Elantra 2017-19", min_enable_speed=19 * CV.MPH_TO_MS, harness=Harness.hyundai_b), CAR.ELANTRA_2021: HyundaiCarInfo("Hyundai Elantra 2021-22", video_link="https://youtu.be/_EdYQtV52-c", harness=Harness.hyundai_k), - CAR.ELANTRA_HEV_2021: HyundaiCarInfo("Hyundai Elantra Hybrid 2021-22", video_link="https://youtu.be/_EdYQtV52-c", harness=Harness.hyundai_k), - CAR.ELANTRA_GT_I30: None, # dashcamOnly and same platform as CAR.ELANTRA + CAR.ELANTRA_HEV_2021: HyundaiCarInfo("Hyundai Elantra Hybrid 2021-23", video_link="https://youtu.be/_EdYQtV52-c", harness=Harness.hyundai_k), + CAR.ELANTRA_GT_I30: [ + HyundaiCarInfo("Hyundai Elantra GT 2017-19", harness=Harness.hyundai_e), + HyundaiCarInfo("Hyundai i30 2019", harness=Harness.hyundai_e), + ], CAR.HYUNDAI_GENESIS: HyundaiCarInfo("Hyundai Genesis 2015-16", min_enable_speed=19 * CV.MPH_TO_MS, harness=Harness.hyundai_j), # TODO: check 2015 packages CAR.IONIQ: HyundaiCarInfo("Hyundai Ioniq Hybrid 2017-19", harness=Harness.hyundai_c), - CAR.IONIQ_HEV_2022: HyundaiCarInfo("Hyundai Ioniq Hybrid 2020-22", "Smart Cruise Control (SCC)", harness=Harness.hyundai_h), # TODO: confirm 2020-21 harness + CAR.IONIQ_HEV_2022: HyundaiCarInfo("Hyundai Ioniq Hybrid 2020-22", harness=Harness.hyundai_h), # TODO: confirm 2020-21 harness CAR.IONIQ_EV_LTD: HyundaiCarInfo("Hyundai Ioniq Electric 2019", harness=Harness.hyundai_c), CAR.IONIQ_EV_2020: HyundaiCarInfo("Hyundai Ioniq Electric 2020", "All", harness=Harness.hyundai_h), CAR.IONIQ_PHEV_2019: HyundaiCarInfo("Hyundai Ioniq Plug-in Hybrid 2019", harness=Harness.hyundai_c), @@ -123,52 +126,58 @@ CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = { CAR.KONA_EV_2022: HyundaiCarInfo("Hyundai Kona Electric 2022", harness=Harness.hyundai_o), CAR.KONA_HEV: HyundaiCarInfo("Hyundai Kona Hybrid 2020", video_link="https://youtu.be/0dwpAHiZgFo", harness=Harness.hyundai_i), # TODO: check packages CAR.SANTA_FE: HyundaiCarInfo("Hyundai Santa Fe 2019-20", "All", harness=Harness.hyundai_d), - CAR.SANTA_FE_2022: HyundaiCarInfo("Hyundai Santa Fe 2021-22", "All", video_link="https://youtu.be/VnHzSTygTS4", harness=Harness.hyundai_l), + CAR.SANTA_FE_2022: HyundaiCarInfo("Hyundai Santa Fe 2021-22", "All", "https://youtu.be/VnHzSTygTS4", harness=Harness.hyundai_l), CAR.SANTA_FE_HEV_2022: HyundaiCarInfo("Hyundai Santa Fe Hybrid 2022", "All", harness=Harness.hyundai_l), CAR.SANTA_FE_PHEV_2022: HyundaiCarInfo("Hyundai Santa Fe Plug-in Hybrid 2022", "All", harness=Harness.hyundai_l), - CAR.SONATA: HyundaiCarInfo("Hyundai Sonata 2020-22", "All", video_link="https://www.youtube.com/watch?v=ix63r9kE3Fw", harness=Harness.hyundai_a), + CAR.SONATA: HyundaiCarInfo("Hyundai Sonata 2020-22", "All", "https://www.youtube.com/watch?v=ix63r9kE3Fw", harness=Harness.hyundai_a), CAR.SONATA_LF: HyundaiCarInfo("Hyundai Sonata 2018-19", harness=Harness.hyundai_e), CAR.TUCSON: [ HyundaiCarInfo("Hyundai Tucson 2021", min_enable_speed=19 * CV.MPH_TO_MS, harness=Harness.hyundai_l), HyundaiCarInfo("Hyundai Tucson Diesel 2019", harness=Harness.hyundai_l), ], CAR.PALISADE: [ - HyundaiCarInfo("Hyundai Palisade 2020-22", "All", video_link="https://youtu.be/TAnDqjF4fDY?t=456", harness=Harness.hyundai_h), + HyundaiCarInfo("Hyundai Palisade 2020-22", "All", "https://youtu.be/TAnDqjF4fDY?t=456", harness=Harness.hyundai_h), HyundaiCarInfo("Kia Telluride 2020", "All", harness=Harness.hyundai_h), ], CAR.VELOSTER: HyundaiCarInfo("Hyundai Veloster 2019-20", min_enable_speed=5. * CV.MPH_TO_MS, harness=Harness.hyundai_e), CAR.SONATA_HYBRID: HyundaiCarInfo("Hyundai Sonata Hybrid 2020-22", "All", harness=Harness.hyundai_a), - CAR.IONIQ_5: HyundaiCarInfo("Hyundai Ioniq 5 2022", "Highway Driving Assist II", harness=Harness.hyundai_q), + CAR.IONIQ_5: [ + HyundaiCarInfo("Hyundai Ioniq 5 (without HDA II) 2022" , "Highway Driving Assist", harness=Harness.hyundai_k), + HyundaiCarInfo("Hyundai Ioniq 5 (with HDA II) 2022", "Highway Driving Assist II", harness=Harness.hyundai_q), + ], CAR.TUCSON_HYBRID_4TH_GEN: HyundaiCarInfo("Hyundai Tucson Hybrid 2022", "All", harness=Harness.hyundai_n), # Kia CAR.KIA_FORTE: HyundaiCarInfo("Kia Forte 2019-21", harness=Harness.hyundai_g), CAR.KIA_K5_2021: HyundaiCarInfo("Kia K5 2021-22", harness=Harness.hyundai_a), CAR.KIA_NIRO_EV: [ - HyundaiCarInfo("Kia Niro EV 2019", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_h), - HyundaiCarInfo("Kia Niro EV 2020", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_f), - HyundaiCarInfo("Kia Niro EV 2021", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_c), - HyundaiCarInfo("Kia Niro EV 2022", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_h), + HyundaiCarInfo("Kia Niro EV 2019", "All", "https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_h), + HyundaiCarInfo("Kia Niro EV 2020", "All", "https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_f), + HyundaiCarInfo("Kia Niro EV 2021", "All", "https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_c), + HyundaiCarInfo("Kia Niro EV 2022", "All", "https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_h), ], CAR.KIA_NIRO_PHEV: HyundaiCarInfo("Kia Niro Plug-in Hybrid 2018-19", "All", min_enable_speed=10. * CV.MPH_TO_MS, harness=Harness.hyundai_c), CAR.KIA_NIRO_HEV_2021: [ HyundaiCarInfo("Kia Niro Hybrid 2021", harness=Harness.hyundai_f), # TODO: could be hyundai_d, verify HyundaiCarInfo("Kia Niro Hybrid 2022", harness=Harness.hyundai_h), ], - CAR.KIA_OPTIMA: HyundaiCarInfo("Kia Optima 2017", "Advanced Smart Cruise Control", harness=Harness.hyundai_b), # TODO: may support 2016, 2018 - CAR.KIA_OPTIMA_2019: HyundaiCarInfo("Kia Optima 2019-20", harness=Harness.hyundai_g), + CAR.KIA_OPTIMA_G4: HyundaiCarInfo("Kia Optima 2017", "Advanced Smart Cruise Control", harness=Harness.hyundai_b), # TODO: may support 2016, 2018 + CAR.KIA_OPTIMA_G4_FL: HyundaiCarInfo("Kia Optima 2019-20", harness=Harness.hyundai_g), CAR.KIA_OPTIMA_H: [ HyundaiCarInfo("Kia Optima Hybrid 2017", "Advanced Smart Cruise Control"), # TODO: may support adjacent years HyundaiCarInfo("Kia Optima Hybrid 2019"), ], CAR.KIA_SELTOS: HyundaiCarInfo("Kia Seltos 2021", harness=Harness.hyundai_a), CAR.KIA_SORENTO: [ - HyundaiCarInfo("Kia Sorento 2018", "Advanced Smart Cruise Control", video_link="https://www.youtube.com/watch?v=Fkh3s6WHJz8", harness=Harness.hyundai_c), + HyundaiCarInfo("Kia Sorento 2018", "Advanced Smart Cruise Control", "https://www.youtube.com/watch?v=Fkh3s6WHJz8", harness=Harness.hyundai_c), HyundaiCarInfo("Kia Sorento 2019", video_link="https://www.youtube.com/watch?v=Fkh3s6WHJz8", harness=Harness.hyundai_e), ], CAR.KIA_STINGER: HyundaiCarInfo("Kia Stinger 2018-20", video_link="https://www.youtube.com/watch?v=MJ94qoofYw0", harness=Harness.hyundai_c), CAR.KIA_CEED: HyundaiCarInfo("Kia Ceed 2019", harness=Harness.hyundai_e), - CAR.KIA_EV6: HyundaiCarInfo("Kia EV6 2022", "Highway Driving Assist II", harness=Harness.hyundai_p), + CAR.KIA_EV6: [ + HyundaiCarInfo("Kia EV6 (without HDA II) 2022", "Highway Driving Assist", harness=Harness.hyundai_l), + HyundaiCarInfo("Kia EV6 (with HDA II) 2022", "Highway Driving Assist II", harness=Harness.hyundai_p) + ], # Genesis CAR.GENESIS_G70: HyundaiCarInfo("Genesis G70 2018-19", "All", harness=Harness.hyundai_f), @@ -290,6 +299,9 @@ FW_QUERY_CONFIG = FwQueryConfig( [HYUNDAI_VERSION_RESPONSE], ), ], + extra_ecus=[ + (Ecu.adas, 0x730, None), # ADAS Driving ECU on HDA2 platforms + ], ) FW_VERSIONS = { @@ -1136,7 +1148,7 @@ FW_VERSIONS = { b'\xf1\x87954A22D200\xf1\x81T01950A1 \xf1\000T0190XBL T01950A1 DSP2T16X4X950NS8\r\xfe\x9c\x8b', ], }, - CAR.KIA_OPTIMA: { + CAR.KIA_OPTIMA_G4: { (Ecu.fwdRadar, 0x7d0, None): [ b'\xf1\x00JF__ SCC F-CUP 1.00 1.00 96400-D4100 ', ], @@ -1150,7 +1162,7 @@ FW_VERSIONS = { b'\xf1\x87\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf1\x816T6J0051\x00\x00\xf1\x006T6J0_C2\x00\x006T6J0051\x00\x00TJF0T20NSB\x00\x00\x00\x00', ], }, - CAR.KIA_OPTIMA_2019: { + CAR.KIA_OPTIMA_G4_FL: { (Ecu.fwdRadar, 0x7d0, None): [ b'\xf1\x00JF__ SCC F-CUP 1.00 1.00 96400-D4110 ', ], @@ -1215,26 +1227,29 @@ FW_VERSIONS = { ], }, CAR.ELANTRA_HEV_2021: { - (Ecu.fwdCamera, 0x7c4, None) : [ + (Ecu.fwdCamera, 0x7c4, None): [ b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.05 99210-AA000 210930', b'\xf1\000CN7HMFC AT USA LHD 1.00 1.03 99210-AA000 200819', + b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.07 99210-AA000 220426', ], - (Ecu.fwdRadar, 0x7d0, None) : [ + (Ecu.fwdRadar, 0x7d0, None): [ b'\xf1\000CNhe SCC FHCUP 1.00 1.01 99110-BY000 ', b'\xf1\x8799110BY000\xf1\x00CNhe SCC FHCUP 1.00 1.01 99110-BY000 ', ], - (Ecu.eps, 0x7d4, None) :[ + (Ecu.eps, 0x7d4, None): [ + b'\xf1\x00CN7 MDPS C 1.00 1.03 56310BY0500 4CNHC103', b'\xf1\x8756310/BY050\xf1\x00CN7 MDPS C 1.00 1.03 56310/BY050 4CNHC103', b'\xf1\x8756310/BY050\xf1\000CN7 MDPS C 1.00 1.02 56310/BY050 4CNHC102', ], - (Ecu.transmission, 0x7e1, None) :[ + (Ecu.transmission, 0x7e1, None): [ b'\xf1\0006U3L0_C2\000\0006U3K3051\000\000HCN0G16NS0\xb9?A\xaa', b'\xf1\0006U3L0_C2\000\0006U3K3051\000\000HCN0G16NS0\000\000\000\000', b'\xf1\x816U3K3051\000\000\xf1\0006U3L0_C2\000\0006U3K3051\000\000HCN0G16NS0\xb9?A\xaa', b'\xf1\x816U3K3051\x00\x00\xf1\x006U3L0_C2\x00\x006U3K3051\x00\x00HCN0G16NS0\x00\x00\x00\x00', ], - (Ecu.engine, 0x7e0, None) : [ + (Ecu.engine, 0x7e0, None): [ b'\xf1\x816H6G5051\x00\x00\x00\x00\x00\x00\x00\x00', + b'\xf1\x816H6G6051\x00\x00\x00\x00\x00\x00\x00\x00', ] }, CAR.KONA_HEV: { @@ -1307,11 +1322,13 @@ FW_VERSIONS = { }, CAR.KIA_EV6: { (Ecu.abs, 0x7d1, None): [ + b'\xf1\x00CV IEB \x02 101!\x10\x18 58520-CV100', b'\xf1\x00CV IEB \x03 101!\x10\x18 58520-CV100', b'\xf1\x8758520CV100\xf1\x00CV IEB \x02 101!\x10\x18 58520-CV100', ], (Ecu.eps, 0x7d4, None): [ b'\xf1\x00CV1 MDPS R 1.00 1.04 57700-CV000 1B30', + b'\xf1\x00CV1 MDPS R 1.00 1.05 57700-CV000 2425', ], (Ecu.fwdRadar, 0x7d0, None): [ b'\xf1\x00CV1_ RDR ----- 1.00 1.01 99110-CV000 ', @@ -1319,6 +1336,7 @@ FW_VERSIONS = { ], (Ecu.fwdCamera, 0x7c4, None): [ b'\xf1\x00CV1 MFC AT USA LHD 1.00 1.05 99210-CV000 211027', + b'\xf1\x00CV1 MFC AT USA LHD 1.00 1.06 99210-CV000 220328', b'\xf1\x00CV1 MFC AT EUR LHD 1.00 1.05 99210-CV000 211027', ], }, @@ -1367,7 +1385,7 @@ CHECKSUM = { FEATURES = { # which message has the gear "use_cluster_gears": {CAR.ELANTRA, CAR.ELANTRA_GT_I30, CAR.KONA}, - "use_tcu_gears": {CAR.KIA_OPTIMA, CAR.KIA_OPTIMA_2019, CAR.SONATA_LF, CAR.VELOSTER, CAR.TUCSON}, + "use_tcu_gears": {CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, CAR.SONATA_LF, CAR.VELOSTER, CAR.TUCSON}, "use_elect_gears": {CAR.KIA_NIRO_EV, CAR.KIA_NIRO_PHEV, CAR.KIA_NIRO_HEV_2021, CAR.KIA_OPTIMA_H, CAR.IONIQ_EV_LTD, CAR.KONA_EV, CAR.IONIQ, CAR.IONIQ_EV_2020, CAR.IONIQ_PHEV, CAR.ELANTRA_HEV_2021, CAR.SONATA_HYBRID, CAR.KONA_HEV, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022, CAR.IONIQ_PHEV_2019, CAR.KONA_EV_2022}, # these cars use the FCA11 message for the AEB and FCW signals, all others use SCC12 @@ -1379,11 +1397,11 @@ CANFD_CAR = {CAR.KIA_EV6, CAR.IONIQ_5, CAR.TUCSON_HYBRID_4TH_GEN} # The camera does SCC on these cars, rather than the radar CAMERA_SCC_CAR = {CAR.KONA_EV_2022, } -HYBRID_CAR = {CAR.IONIQ_PHEV, CAR.ELANTRA_HEV_2021, CAR.KIA_NIRO_PHEV, CAR.KIA_NIRO_HEV_2021, CAR.SONATA_HYBRID, CAR.KONA_HEV, CAR.IONIQ, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022, CAR.IONIQ_PHEV_2019} # these cars use a different gas signal -EV_CAR = {CAR.IONIQ_EV_2020, CAR.IONIQ_EV_LTD, CAR.KONA_EV, CAR.KIA_NIRO_EV, CAR.KONA_EV_2022} +HYBRID_CAR = {CAR.IONIQ_PHEV, CAR.ELANTRA_HEV_2021, CAR.KIA_NIRO_PHEV, CAR.KIA_NIRO_HEV_2021, CAR.SONATA_HYBRID, CAR.KONA_HEV, CAR.IONIQ, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022, CAR.IONIQ_PHEV_2019, CAR.TUCSON_HYBRID_4TH_GEN} # these cars use a different gas signal +EV_CAR = {CAR.IONIQ_EV_2020, CAR.IONIQ_EV_LTD, CAR.KONA_EV, CAR.KIA_NIRO_EV, CAR.KONA_EV_2022, CAR.KIA_EV6, CAR.IONIQ_5} # these cars require a special panda safety mode due to missing counters and checksums in the messages -LEGACY_SAFETY_MODE_CAR = {CAR.HYUNDAI_GENESIS, CAR.IONIQ_EV_2020, CAR.IONIQ_EV_LTD, CAR.IONIQ_PHEV, CAR.IONIQ, CAR.KONA_EV, CAR.KIA_SORENTO, CAR.SONATA_LF, CAR.KIA_OPTIMA, CAR.KIA_OPTIMA_2019, CAR.VELOSTER, CAR.KIA_STINGER, CAR.GENESIS_G70, CAR.GENESIS_G80, CAR.KIA_CEED, CAR.ELANTRA, CAR.IONIQ_HEV_2022} +LEGACY_SAFETY_MODE_CAR = {CAR.HYUNDAI_GENESIS, CAR.IONIQ_EV_2020, CAR.IONIQ_EV_LTD, CAR.IONIQ_PHEV, CAR.IONIQ, CAR.KONA_EV, CAR.KIA_SORENTO, CAR.SONATA_LF, CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, CAR.VELOSTER, CAR.KIA_STINGER, CAR.GENESIS_G70, CAR.GENESIS_G80, CAR.KIA_CEED, CAR.ELANTRA, CAR.IONIQ_HEV_2022} # If 0x500 is present on bus 1 it probably has a Mando radar outputting radar points. # If no points are outputted by default it might be possible to turn it on using selfdrive/debug/hyundai_enable_radar_points.py @@ -1408,8 +1426,8 @@ DBC = { CAR.KIA_NIRO_EV: dbc_dict('hyundai_kia_generic', 'hyundai_kia_mando_front_radar_generated'), CAR.KIA_NIRO_PHEV: dbc_dict('hyundai_kia_generic', 'hyundai_kia_mando_front_radar_generated'), CAR.KIA_NIRO_HEV_2021: dbc_dict('hyundai_kia_generic', None), - CAR.KIA_OPTIMA: dbc_dict('hyundai_kia_generic', None), - CAR.KIA_OPTIMA_2019: dbc_dict('hyundai_kia_generic', None), + CAR.KIA_OPTIMA_G4: dbc_dict('hyundai_kia_generic', None), + CAR.KIA_OPTIMA_G4_FL: dbc_dict('hyundai_kia_generic', None), CAR.KIA_OPTIMA_H: dbc_dict('hyundai_kia_generic', None), CAR.KIA_SELTOS: dbc_dict('hyundai_kia_generic', None), CAR.KIA_SORENTO: dbc_dict('hyundai_kia_generic', None), # Has 0x5XX messages, but different format diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index a789fc6cad..4647a04244 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -10,11 +10,12 @@ from common.conversions import Conversions as CV from common.kalman.simple_kalman import KF1D from common.numpy_fast import interp from common.realtime import DT_CTRL -from selfdrive.car import apply_hysteresis, create_button_enable_events, gen_empty_fingerprint +from selfdrive.car import apply_hysteresis, gen_empty_fingerprint from selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX, apply_deadzone from selfdrive.controls.lib.events import Events from selfdrive.controls.lib.vehicle_model import VehicleModel +ButtonType = car.CarState.ButtonEvent.Type GearShifter = car.CarState.GearShifter EventName = car.CarEvent.EventName TorqueFromLateralAccelCallbackType = Callable[[float, car.CarParams.LateralTorqueTuning, float, float, bool], float] @@ -210,7 +211,8 @@ class CarInterfaceBase(ABC): def apply(self, c: car.CarControl) -> Tuple[car.CarControl.Actuators, List[bytes]]: pass - def create_common_events(self, cs_out, extra_gears=None, pcm_enable=True, allow_enable=True): + def create_common_events(self, cs_out, extra_gears=None, pcm_enable=True, allow_enable=True, + enable_buttons=(ButtonType.accelCruise, ButtonType.decelCruise)): events = Events() if cs_out.doorOpen: @@ -244,7 +246,13 @@ class CarInterfaceBase(ABC): events.add(EventName.steerOverride) # Handle button presses - events.events.extend(create_button_enable_events(cs_out.buttonEvents, pcm_cruise=self.CP.pcmCruise)) + for b in cs_out.buttonEvents: + # Enable OP long on falling edge of enable buttons (defaults to accelCruise and decelCruise, overridable per-port) + if not self.CP.pcmCruise and (b.type in enable_buttons and not b.pressed): + events.add(EventName.buttonEnable) + # Disable on rising edge of cancel for both stock and OP long + if b.type == ButtonType.cancel and b.pressed: + events.add(EventName.buttonCancel) # Handle permanent and temporary steering faults self.steering_unpressed = 0 if cs_out.steeringPressed else self.steering_unpressed + 1 diff --git a/selfdrive/car/isotp_parallel_query.py b/selfdrive/car/isotp_parallel_query.py index 65122ab897..4b4bdcc0ca 100644 --- a/selfdrive/car/isotp_parallel_query.py +++ b/selfdrive/car/isotp_parallel_query.py @@ -1,7 +1,6 @@ import time from collections import defaultdict from functools import partial -from typing import Optional import cereal.messaging as messaging from system.swaglog import cloudlog @@ -10,24 +9,21 @@ from panda.python.uds import CanClient, IsoTpMessage, FUNCTIONAL_ADDRS, get_rx_a class IsoTpParallelQuery: - def __init__(self, sendcan, logcan, bus, addrs, request, response, response_offset=0x8, functional_addr=False, debug=False, response_pending_timeout=10): + def __init__(self, sendcan, logcan, bus, addrs, request, response, response_offset=0x8, functional_addrs=None, debug=False, response_pending_timeout=10): self.sendcan = sendcan self.logcan = logcan self.bus = bus self.request = request self.response = response + self.functional_addrs = functional_addrs or [] self.debug = debug - self.functional_addr = functional_addr self.response_pending_timeout = response_pending_timeout - self.real_addrs = [] - for a in addrs: - if isinstance(a, tuple): - self.real_addrs.append(a) - else: - self.real_addrs.append((a, None)) + real_addrs = [a if isinstance(a, tuple) else (a, None) for a in addrs] + for tx_addr, _ in real_addrs: + assert tx_addr not in 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 self.real_addrs} + self.msg_addrs = {tx_addr: get_rx_addr_for_tx_addr(tx_addr[0], rx_offset=response_offset) for tx_addr in real_addrs} self.msg_buffer = defaultdict(list) def rx(self): @@ -36,13 +32,8 @@ class IsoTpParallelQuery: for packet in can_packets: for msg in packet.can: - if msg.src == self.bus: - if self.functional_addr: - if (0x7E8 <= msg.address <= 0x7EF) or (0x18DAF100 <= msg.address <= 0x18DAF1FF): - fn_addr = next(a for a in FUNCTIONAL_ADDRS if msg.address - a <= 32) - self.msg_buffer[fn_addr].append((msg.address, msg.busTime, msg.dat, msg.src)) - elif msg.address in self.msg_addrs.values(): - self.msg_buffer[msg.address].append((msg.address, msg.busTime, msg.dat, msg.src)) + if msg.src == self.bus and msg.address in self.msg_addrs.values(): + self.msg_buffer[msg.address].append((msg.address, msg.busTime, msg.dat, msg.src)) def _can_tx(self, tx_addr, dat, bus): """Helper function to send single message""" @@ -72,6 +63,16 @@ class IsoTpParallelQuery: messaging.drain_sock(self.logcan) self.msg_buffer = defaultdict(list) + def _create_isotp_msg(self, tx_addr, sub_addr, rx_addr): + can_client = CanClient(self._can_tx, partial(self._can_rx, rx_addr, sub_addr=sub_addr), tx_addr, rx_addr, + self.bus, sub_addr=sub_addr, debug=self.debug) + + max_len = 8 if sub_addr is None else 7 + # uses iso-tp frame separation time of 10 ms + # TODO: use single_frame_mode so ECUs can send as fast as they want, + # as well as reduces chances we process messages from previous queries + return IsoTpMessage(can_client, timeout=0, separation_time=0.01, debug=self.debug, max_len=max_len) + def get_data(self, timeout, total_timeout=60.): self._drain_rx() @@ -80,22 +81,19 @@ class IsoTpParallelQuery: request_counter = {} request_done = {} for tx_addr, rx_addr in self.msg_addrs.items(): - # rx_addr not set when using functional tx addr - id_addr = rx_addr or tx_addr[0] - sub_addr = tx_addr[1] - - can_client = CanClient(self._can_tx, partial(self._can_rx, id_addr, sub_addr=sub_addr), tx_addr[0], rx_addr, - self.bus, sub_addr=sub_addr, debug=self.debug) - - max_len = 8 if sub_addr is None else 7 - - msg = IsoTpMessage(can_client, timeout=0, max_len=max_len, debug=self.debug) - msg.send(self.request[0]) - - msgs[tx_addr] = msg + msgs[tx_addr] = self._create_isotp_msg(*tx_addr, rx_addr) request_counter[tx_addr] = 0 request_done[tx_addr] = False + # Send first request to functional addrs, subsequent responses are handled on physical addrs + if len(self.functional_addrs): + for addr in self.functional_addrs: + self._create_isotp_msg(addr, None, -1).send(self.request[0]) + + # If querying functional addrs, set up physical IsoTpMessages to send consecutive frames + for msg in msgs.values(): + msg.send(self.request[0], setup_only=len(self.functional_addrs) > 0) + results = {} start_time = time.monotonic() response_timeouts = {tx_addr: start_time + timeout for tx_addr in self.msg_addrs} @@ -107,12 +105,15 @@ class IsoTpParallelQuery: for tx_addr, msg in msgs.items(): try: - dat: Optional[bytes] = msg.recv() + dat, updated = msg.recv() except Exception: - cloudlog.exception("Error processing UDS response") + cloudlog.exception(f"Error processing UDS response: {tx_addr}") request_done[tx_addr] = True continue + if updated: + response_timeouts[tx_addr] = time.monotonic() + timeout + if not dat: continue @@ -121,12 +122,11 @@ class IsoTpParallelQuery: response_valid = dat[:len(expected_response)] == expected_response if response_valid: - response_timeouts[tx_addr] = time.monotonic() + timeout if counter + 1 < len(self.request): msg.send(self.request[counter + 1]) request_counter[tx_addr] += 1 else: - results[(tx_addr, msg._can_client.rx_addr)] = dat[len(expected_response):] + results[tx_addr] = dat[len(expected_response):] request_done[tx_addr] = True else: error_code = dat[2] if len(dat) > 2 else -1 diff --git a/selfdrive/car/mock/interface.py b/selfdrive/car/mock/interface.py index 77e3703c2d..a3194cd79e 100755 --- a/selfdrive/car/mock/interface.py +++ b/selfdrive/car/mock/interface.py @@ -20,8 +20,7 @@ class CarInterface(CarInterfaceBase): cloudlog.debug("Using Mock Car Interface") - self.sensor = messaging.sub_sock('sensorEvents') - self.gps = messaging.sub_sock('gpsLocationExternal') + self.sm = messaging.SubMaster(['gyroscope', 'gpsLocation', 'gpsLocationExternal']) self.speed = 0. self.prev_speed = 0. @@ -45,17 +44,16 @@ class CarInterface(CarInterfaceBase): # returns a car.CarState def _update(self, c): + self.sm.update(0) + # get basic data from phone and gps since CAN isn't connected - sensors = messaging.recv_sock(self.sensor) - if sensors is not None: - for sensor in sensors.sensorEvents: - if sensor.type == 4: # gyro - self.yaw_rate_meas = -sensor.gyro.v[0] - - gps = messaging.recv_sock(self.gps) - if gps is not None: + if self.sm.updated['gyroscope']: + self.yaw_rate_meas = -self.sm['gyroscope'].gyroUncalibrated.v[0] + + gps_sock = 'gpsLocationExternal' if self.sm.rcv_frame['gpsLocationExternal'] > 1 else 'gpsLocation' + if self.sm.updated[gps_sock]: self.prev_speed = self.speed - self.speed = gps.gpsLocationExternal.speed + self.speed = self.sm[gps_sock].speed # create message ret = car.CarState.new_message() diff --git a/selfdrive/car/subaru/carstate.py b/selfdrive/car/subaru/carstate.py index 128e4245b2..ba873c48d7 100644 --- a/selfdrive/car/subaru/carstate.py +++ b/selfdrive/car/subaru/carstate.py @@ -58,9 +58,6 @@ class CarState(CarStateBase): ret.cruiseState.available = cp_cruise.vl["CruiseControl"]["Cruise_On"] != 0 ret.cruiseState.speed = cp_cam.vl["ES_DashStatus"]["Cruise_Set_Speed"] * CV.KPH_TO_MS - if self.car_fingerprint not in PREGLOBAL_CARS: - ret.cruiseState.standstill = cp_cam.vl["ES_DashStatus"]["Cruise_State"] == 3 - if (self.car_fingerprint in PREGLOBAL_CARS and cp.vl["Dash_State2"]["UNITS"] == 1) or \ (self.car_fingerprint not in PREGLOBAL_CARS and cp.vl["Dashlights"]["UNITS"] == 1): ret.cruiseState.speed *= CV.MPH_TO_KPH @@ -78,6 +75,8 @@ class CarState(CarStateBase): else: ret.steerFaultTemporary = cp.vl["Steering_Torque"]["Steer_Warning"] == 1 ret.cruiseState.nonAdaptive = cp_cam.vl["ES_DashStatus"]["Conventional_Cruise"] == 1 + ret.cruiseState.standstill = cp_cam.vl["ES_DashStatus"]["Cruise_State"] == 3 + ret.stockFcw = cp_cam.vl["ES_LKAS_State"]["LKAS_Alert"] == 2 self.es_lkas_msg = copy.copy(cp_cam.vl["ES_LKAS_State"]) cp_es_distance = cp_body if self.car_fingerprint in GLOBAL_GEN2 else cp_cam diff --git a/selfdrive/car/subaru/values.py b/selfdrive/car/subaru/values.py index 6c3a35307e..da5ff1785a 100644 --- a/selfdrive/car/subaru/values.py +++ b/selfdrive/car/subaru/values.py @@ -124,23 +124,26 @@ FW_VERSIONS = { CAR.LEGACY: { (Ecu.abs, 0x7b0, None): [ b'\xa1\\ x04\x01', - b'\xa1 \x03\x03' + b'\xa1 \x03\x03', + b'\xa1 \x02\x01', ], (Ecu.eps, 0x746, None): [ b'\x9b\xc0\x11\x00', - b'\x9b\xc0\x11\x02' + b'\x9b\xc0\x11\x02', ], (Ecu.fwdCamera, 0x787, None): [ b'\x00\x00e\x80\x00\x1f@ \x19\x00', - b'\x00\x00e\x9a\x00\x00\x00\x00\x00\x00' + b'\x00\x00e\x9a\x00\x00\x00\x00\x00\x00', ], (Ecu.engine, 0x7e0, None): [ b'\xde\"a0\x07', - b'\xe2"aq\x07' + b'\xe2"aq\x07', + b'\xde,\xa0@\x07', ], (Ecu.transmission, 0x7e1, None): [ b'\xa5\xf6\x05@\x00', - b'\xa7\xf6\x04@\x00' + b'\xa7\xf6\x04@\x00', + b'\xa5\xfe\xc7@\x00', ], }, CAR.IMPREZA: { diff --git a/selfdrive/car/tesla/values.py b/selfdrive/car/tesla/values.py index 7648a4a504..e28666c625 100644 --- a/selfdrive/car/tesla/values.py +++ b/selfdrive/car/tesla/values.py @@ -1,9 +1,12 @@ from collections import namedtuple from typing import Dict, List, Union +from cereal import car from selfdrive.car import dbc_dict from selfdrive.car.docs_definitions import CarInfo -from cereal import car +from selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries + +Ecu = car.CarParams.Ecu Button = namedtuple('Button', ['event_type', 'can_addr', 'can_msg', 'values']) AngleRateLimit = namedtuple('AngleRateLimit', ['speed_points', 'max_angle_diff_points']) @@ -20,11 +23,6 @@ CAR_INFO: Dict[str, Union[CarInfo, List[CarInfo]]] = { } FINGERPRINTS = { - CAR.AP2_MODELS: [ - { - 1: 8, 3: 8, 14: 8, 21: 4, 69: 8, 109: 4, 257: 3, 264: 8, 277: 6, 280: 6, 293: 4, 296: 4, 309: 5, 325: 8, 328: 5, 336: 8, 341: 8, 360: 7, 373: 8, 389: 8, 415: 8, 513: 5, 516: 8, 518: 8, 520: 4, 522: 8, 524: 8, 526: 8, 532: 3, 536: 8, 537: 3, 538: 8, 542: 8, 551: 5, 552: 2, 556: 8, 558: 8, 568: 8, 569: 8, 574: 8, 576: 3, 577: 8, 582: 5, 583: 8, 584: 4, 585: 8, 590: 8, 601: 8, 606: 8, 608: 1, 622: 8, 627: 6, 638: 8, 641: 8, 643: 8, 692: 8, 693: 8, 695: 8, 696: 8, 697: 8, 699: 8, 700: 8, 701: 8, 702: 8, 703: 8, 704: 8, 708: 8, 709: 8, 710: 8, 711: 8, 712: 8, 728: 8, 744: 8, 760: 8, 772: 8, 775: 8, 776: 8, 777: 8, 778: 8, 782: 8, 788: 8, 791: 8, 792: 8, 796: 2, 797: 8, 798: 6, 799: 8, 804: 8, 805: 8, 807: 8, 808: 1, 811: 8, 812: 8, 813: 8, 814: 5, 815: 8, 820: 8, 823: 8, 824: 8, 829: 8, 830: 5, 836: 8, 840: 8, 845: 8, 846: 5, 848: 8, 852: 8, 853: 8, 856: 4, 857: 6, 861: 8, 862: 5, 872: 8, 876: 8, 877: 8, 879: 8, 880: 8, 882: 8, 884: 8, 888: 8, 893: 8, 894: 8, 901: 6, 904: 3, 905: 8, 906: 8, 908: 2, 909: 8, 910: 8, 912: 8, 920: 8, 921: 8, 925: 4, 926: 6, 936: 8, 941: 8, 949: 8, 952: 8, 953: 6, 968: 8, 969: 7, 970: 8, 971: 8, 977: 8, 984: 8, 986: 8, 987: 8, 990: 8, 1000: 8, 1001: 8, 1006: 8, 1007: 8, 1008: 8, 1010: 6, 1014: 1, 1015: 8, 1016: 8, 1017: 8, 1018: 8, 1020: 8, 1026: 8, 1028: 8, 1029: 8, 1030: 8, 1032: 1, 1033: 1, 1034: 8, 1048: 1, 1049: 8, 1061: 8, 1064: 8, 1065: 8, 1070: 8, 1080: 8, 1081: 8, 1097: 8, 1113: 8, 1129: 8, 1145: 8, 1160: 4, 1177: 8, 1281: 8, 1328: 8, 1329: 8, 1332: 8, 1335: 8, 1337: 8, 1353: 8, 1368: 8, 1412: 8, 1436: 8, 1476: 8, 1481: 8, 1497: 8, 1513: 8, 1519: 8, 1601: 8, 1605: 8, 1617: 8, 1621: 8, 1625: 8, 1665: 8, 1792: 8, 1798: 8, 1800: 4, 1804: 8, 1812: 8, 1815: 8, 1816: 8, 1824: 8, 1825: 8, 1828: 8, 1831: 8, 1832: 8, 1840: 8, 1842: 8, 1848: 8, 1864: 8, 1872: 8, 1880: 8, 1888: 8, 1892: 8, 1896: 8, 1912: 8, 1937: 8, 1953: 8, 1960: 8, 1968: 8, 1992: 8, 2001: 8, 2008: 3, 2015: 8, 2016: 8, 2043: 5, 2045: 4 - }, - ], CAR.AP1_MODELS: [ { 1: 8, 3: 8, 14: 8, 21: 4, 69: 8, 109: 4, 257: 3, 264: 8, 267: 5, 277: 6, 280: 6, 283: 5, 293: 4, 296: 4, 309: 5, 325: 8, 328: 5, 336: 8, 341: 8, 360: 7, 373: 8, 389: 8, 415: 8, 513: 5, 516: 8, 520: 4, 522: 8, 524: 8, 526: 8, 532: 3, 536: 8, 537: 3, 542: 8, 551: 5, 552: 2, 556: 8, 558: 8, 568: 8, 569: 8, 574: 8, 577: 8, 582: 5, 584: 4, 585: 8, 590: 8, 606: 8, 622: 8, 627: 6, 638: 8, 641: 8, 643: 8, 660: 5, 693: 8, 696: 8, 697: 8, 712: 8, 728: 8, 744: 8, 760: 8, 772: 8, 775: 8, 776: 8, 777: 8, 778: 8, 782: 8, 788: 8, 791: 8, 792: 8, 796: 2, 797: 8, 798: 6, 799: 8, 804: 8, 805: 8, 807: 8, 808: 1, 809: 8, 812: 8, 813: 8, 814: 5, 815: 8, 820: 8, 823: 8, 824: 8, 829: 8, 830: 5, 836: 8, 840: 8, 841: 8, 845: 8, 846: 5, 852: 8, 856: 4, 857: 6, 861: 8, 862: 5, 872: 8, 873: 8, 877: 8, 878: 8, 879: 8, 880: 8, 884: 8, 888: 8, 889: 8, 893: 8, 896: 8, 901: 6, 904: 3, 905: 8, 908: 2, 909: 8, 920: 8, 921: 8, 925: 4, 936: 8, 937: 8, 941: 8, 949: 8, 952: 8, 953: 6, 957: 8, 968: 8, 973: 8, 984: 8, 987: 8, 989: 8, 990: 8, 1000: 8, 1001: 8, 1006: 8, 1016: 8, 1026: 8, 1028: 8, 1029: 8, 1030: 8, 1032: 1, 1033: 1, 1034: 8, 1048: 1, 1064: 8, 1070: 8, 1080: 8, 1160: 4, 1281: 8, 1329: 8, 1332: 8, 1335: 8, 1337: 8, 1368: 8, 1412: 8, 1436: 8, 1465: 8, 1476: 8, 1497: 8, 1524: 8, 1527: 8, 1601: 8, 1605: 8, 1611: 8, 1614: 8, 1617: 8, 1621: 8, 1627: 8, 1630: 8, 1800: 4, 1804: 8, 1812: 8, 1815: 8, 1816: 8, 1828: 8, 1831: 8, 1832: 8, 1840: 8, 1848: 8, 1864: 8, 1880: 8, 1892: 8, 1896: 8, 1912: 8, 1960: 8, 1992: 8, 2008: 3, 2043: 5, 2045: 4 @@ -37,6 +35,42 @@ DBC = { CAR.AP1_MODELS: dbc_dict('tesla_powertrain', 'tesla_radar', chassis_dbc='tesla_can'), } +FW_QUERY_CONFIG = FwQueryConfig( + requests=[ + Request( + [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.UDS_VERSION_REQUEST], + [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.UDS_VERSION_RESPONSE], + whitelist_ecus=[Ecu.eps], + rx_offset=0x08, + bus=0, + ), + Request( + [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.UDS_VERSION_REQUEST], + [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.UDS_VERSION_RESPONSE], + whitelist_ecus=[Ecu.adas, Ecu.electricBrakeBooster, Ecu.fwdRadar], + rx_offset=0x10, + bus=0, + ), + ] +) + +FW_VERSIONS = { + CAR.AP2_MODELS: { + (Ecu.adas, 0x649, None): [ + b'\x01\x00\x8b\x07\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11', + ], + (Ecu.electricBrakeBooster, 0x64d, None): [ + b'1037123-00-A', + ], + (Ecu.fwdRadar, 0x671, None): [ + b'\x01\x00W\x00\x00\x00\x07\x00\x00\x00\x00\x08\x01\x00\x00\x00\x07\xff\xfe', + ], + (Ecu.eps, 0x730, None): [ + b'\x10#\x01', + ], + }, +} + class CANBUS: # Lateral harness chassis = 0 diff --git a/selfdrive/car/tests/routes.py b/selfdrive/car/tests/routes.py index 3ae3a357ce..85787ae88d 100644 --- a/selfdrive/car/tests/routes.py +++ b/selfdrive/car/tests/routes.py @@ -88,8 +88,8 @@ routes = [ CarTestRoute("37398f32561a23ad|2021-11-18--00-11-35", HYUNDAI.SANTA_FE_HEV_2022), CarTestRoute("656ac0d830792fcc|2021-12-28--14-45-56", HYUNDAI.SANTA_FE_PHEV_2022, segment=1), CarTestRoute("e0e98335f3ebc58f|2021-03-07--16-38-29", HYUNDAI.KIA_CEED), - CarTestRoute("7653b2bce7bcfdaa|2020-03-04--15-34-32", HYUNDAI.KIA_OPTIMA), - CarTestRoute("018654717bc93d7d|2022-09-19--23-11-10", HYUNDAI.KIA_OPTIMA_2019, segment=0), + CarTestRoute("7653b2bce7bcfdaa|2020-03-04--15-34-32", HYUNDAI.KIA_OPTIMA_G4), + CarTestRoute("018654717bc93d7d|2022-09-19--23-11-10", HYUNDAI.KIA_OPTIMA_G4_FL, segment=0), CarTestRoute("c75a59efa0ecd502|2021-03-11--20-52-55", HYUNDAI.KIA_SELTOS), CarTestRoute("5b7c365c50084530|2020-04-15--16-13-24", HYUNDAI.SONATA), CarTestRoute("b2a38c712dcf90bd|2020-05-18--18-12-48", HYUNDAI.SONATA_LF), @@ -110,7 +110,8 @@ routes = [ CarTestRoute("49f3c13141b6bc87|2021-07-28--08-05-13", HYUNDAI.KONA_HEV), CarTestRoute("5dddcbca6eb66c62|2020-07-26--13-24-19", HYUNDAI.KIA_STINGER), CarTestRoute("d624b3d19adce635|2020-08-01--14-59-12", HYUNDAI.VELOSTER), - CarTestRoute("d824e27e8c60172c|2022-05-19--16-15-28", HYUNDAI.KIA_EV6), + CarTestRoute("d824e27e8c60172c|2022-05-19--16-15-28", HYUNDAI.KIA_EV6), # HDA2 + CarTestRoute("68d6a96e703c00c9|2022-09-10--16-09-39", HYUNDAI.KIA_EV6), # HDA1 CarTestRoute("007d5e4ad9f86d13|2021-09-30--15-09-23", HYUNDAI.KIA_K5_2021), CarTestRoute("50c6c9b85fd1ff03|2020-10-26--17-56-06", HYUNDAI.KIA_NIRO_EV), CarTestRoute("173219cf50acdd7b|2021-07-05--10-27-41", HYUNDAI.KIA_NIRO_PHEV), @@ -175,6 +176,7 @@ routes = [ CarTestRoute("4d134e099430fba2|2021-03-26--00-26-06", VOLKSWAGEN.PASSAT_MK8), CarTestRoute("3cfdec54aa035f3f|2022-07-19--23-45-10", VOLKSWAGEN.PASSAT_NMS), CarTestRoute("0cd0b7f7e31a3853|2021-11-03--19-30-22", VOLKSWAGEN.POLO_MK6), + CarTestRoute("064d1816e448f8eb|2022-09-29--15-32-34", VOLKSWAGEN.SHARAN_MK2), CarTestRoute("7d82b2f3a9115f1f|2021-10-21--15-39-42", VOLKSWAGEN.TAOS_MK1), CarTestRoute("2744c89a8dda9a51|2021-07-24--21-28-06", VOLKSWAGEN.TCROSS_MK1), CarTestRoute("2cef8a0b898f331a|2021-03-25--20-13-57", VOLKSWAGEN.TIGUAN_MK2), diff --git a/selfdrive/car/tests/test_docs.py b/selfdrive/car/tests/test_docs.py index 191b36b8f2..b7056df5b3 100755 --- a/selfdrive/car/tests/test_docs.py +++ b/selfdrive/car/tests/test_docs.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +from collections import defaultdict import re import unittest @@ -20,6 +21,15 @@ class TestCarDocs(unittest.TestCase): self.assertEqual(generated_cars_md, current_cars_md, "Run selfdrive/car/docs.py to update the compatibility documentation") + def test_duplicate_years(self): + make_model_years = defaultdict(list) + for car in self.all_cars: + with self.subTest(car_info_name=car.name): + make_model = (car.make, car.model) + for year in car.year_list: + self.assertNotIn(year, make_model_years[make_model], f"{car.name}: Duplicate model year") + make_model_years[make_model].append(year) + def test_missing_car_info(self): all_car_info_platforms = get_interface_attr("CAR_INFO", combine_brands=True).keys() for platform in sorted(interfaces.keys()): diff --git a/selfdrive/car/tests/test_fw_fingerprint.py b/selfdrive/car/tests/test_fw_fingerprint.py index f0d2744a98..ed323b0563 100755 --- a/selfdrive/car/tests/test_fw_fingerprint.py +++ b/selfdrive/car/tests/test_fw_fingerprint.py @@ -44,6 +44,13 @@ class TestFwFingerprint(unittest.TestCase): duplicates = {fw for fw in ecu_fw if ecu_fw.count(fw) > 1} self.assertFalse(len(duplicates), f"{car_model}: Duplicate FW versions: Ecu.{ECU_NAME[ecu[0]]}, {duplicates}") + def test_data_collection_ecus(self): + for brand, config in FW_QUERY_CONFIGS.items(): + for car_model, ecus in VERSIONS[brand].items(): + bad_ecus = set(ecus).intersection(config.extra_ecus) + with self.subTest(car_model=car_model): + self.assertFalse(len(bad_ecus), f'{car_model}: Fingerprints contain ECUs added for data collection: {bad_ecus}') + def test_blacklisted_ecus(self): blacklisted_addrs = (0x7c4, 0x7d0) # includes A/C ecu and an unknown ecu for car_model, ecus in FW_VERSIONS.items(): diff --git a/selfdrive/car/torque_data/override.yaml b/selfdrive/car/torque_data/override.yaml index b5d1a31193..889eeffb25 100644 --- a/selfdrive/car/torque_data/override.yaml +++ b/selfdrive/car/torque_data/override.yaml @@ -30,6 +30,7 @@ CHEVROLET BOLT EUV 2022: [2.0, 2.0, 0.05] CHEVROLET SILVERADO 1500 2020: [1.9, 1.9, 0.112] CHEVROLET EQUINOX 2019: [2.0, 2.0, 0.05] VOLKSWAGEN PASSAT NMS: [2.5, 2.5, 0.1] +VOLKSWAGEN SHARAN 2ND GEN: [2.5, 2.5, 0.1] HYUNDAI TUCSON HYBRID 4TH GEN: [2.5, 2.5, 0.0] # Dashcam or fallback configured as ideal car diff --git a/selfdrive/car/torque_data/substitute.yaml b/selfdrive/car/torque_data/substitute.yaml index 92361d37f4..f5e3d1d61d 100644 --- a/selfdrive/car/torque_data/substitute.yaml +++ b/selfdrive/car/torque_data/substitute.yaml @@ -17,8 +17,8 @@ LEXUS RC 2020: LEXUS NX 2020 TOYOTA AVALON HYBRID 2019: TOYOTA AVALON 2019 TOYOTA AVALON HYBRID 2022: TOYOTA AVALON 2022 -KIA OPTIMA 2016: HYUNDAI SONATA 2020 -KIA OPTIMA 2019: HYUNDAI SONATA 2020 +KIA OPTIMA 4TH GEN: HYUNDAI SONATA 2020 +KIA OPTIMA 4TH GEN FACELIFT: HYUNDAI SONATA 2020 KIA OPTIMA HYBRID 2017 & SPORTS 2019: HYUNDAI SONATA 2020 KIA FORTE E 2018 & GT 2021: HYUNDAI SONATA 2020 KIA CEED INTRO ED 2019: HYUNDAI SONATA 2020 diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 26c61f5b74..819b85d759 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -136,7 +136,7 @@ CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { CAR.HIGHLANDERH: ToyotaCarInfo("Toyota Highlander Hybrid 2017-19", footnotes=[Footnote.DSU]), CAR.HIGHLANDERH_TSS2: ToyotaCarInfo("Toyota Highlander Hybrid 2020-22"), CAR.PRIUS: [ - ToyotaCarInfo("Toyota Prius 2016", "Toyota Safety Sense P", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0", footnotes=[Footnote.DSU]), + ToyotaCarInfo("Toyota Prius 2016", "Toyota Safety Sense P", "https://www.youtube.com/watch?v=8zopPJI8XQ0", [Footnote.DSU]), ToyotaCarInfo("Toyota Prius 2017-20", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0", footnotes=[Footnote.DSU]), ToyotaCarInfo("Toyota Prius Prime 2017-20", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0", footnotes=[Footnote.DSU]), ], @@ -150,7 +150,7 @@ CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { ToyotaCarInfo("Toyota RAV4 2017-18", footnotes=[Footnote.DSU]) ], CAR.RAV4H: [ - ToyotaCarInfo("Toyota RAV4 Hybrid 2016", "Toyota Safety Sense P", video_link="https://youtu.be/LhT5VzJVfNI?t=26", footnotes=[Footnote.DSU]), + ToyotaCarInfo("Toyota RAV4 Hybrid 2016", "Toyota Safety Sense P", "https://youtu.be/LhT5VzJVfNI?t=26", [Footnote.DSU]), ToyotaCarInfo("Toyota RAV4 Hybrid 2017-18", video_link="https://youtu.be/LhT5VzJVfNI?t=26", footnotes=[Footnote.DSU]) ], CAR.RAV4_TSS2: ToyotaCarInfo("Toyota RAV4 2019-21", video_link="https://www.youtube.com/watch?v=wJxjDd42gGA"), @@ -171,8 +171,14 @@ CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { CAR.LEXUS_NX_TSS2: ToyotaCarInfo("Lexus NX 2020-21"), CAR.LEXUS_NXH_TSS2: ToyotaCarInfo("Lexus NX Hybrid 2020-21"), CAR.LEXUS_RC: ToyotaCarInfo("Lexus RC 2017-20"), - CAR.LEXUS_RX: ToyotaCarInfo("Lexus RX 2016-19", footnotes=[Footnote.DSU]), - CAR.LEXUS_RXH: ToyotaCarInfo("Lexus RX Hybrid 2016-19", footnotes=[Footnote.DSU]), + CAR.LEXUS_RX: [ + ToyotaCarInfo("Lexus RX 2016", "Lexus Safety System+", footnotes=[Footnote.DSU]), + ToyotaCarInfo("Lexus RX 2017-19", footnotes=[Footnote.DSU]), + ], + CAR.LEXUS_RXH: [ + ToyotaCarInfo("Lexus RX Hybrid 2016", "Lexus Safety System+", footnotes=[Footnote.DSU]), + ToyotaCarInfo("Lexus RX Hybrid 2017-19", footnotes=[Footnote.DSU]), + ], CAR.LEXUS_RX_TSS2: ToyotaCarInfo("Lexus RX 2020-22"), CAR.LEXUS_RXH_TSS2: ToyotaCarInfo("Lexus RX Hybrid 2020-21"), } @@ -751,6 +757,7 @@ FW_VERSIONS = { b'\x03312N6000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203402\x00\x00\x00\x00', b'\x03312N6100\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203302\x00\x00\x00\x00', b'\x03312N6100\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203402\x00\x00\x00\x00', + b'\x03312N6200\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203202\x00\x00\x00\x00', b'\x03312N6200\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203302\x00\x00\x00\x00', b'\x02312K4000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02312U5000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00', @@ -1356,6 +1363,7 @@ FW_VERSIONS = { ], (Ecu.engine, 0x700, None): [ b'\x01896634AA0000\x00\x00\x00\x00', + b'\x01896634AA0100\x00\x00\x00\x00', b'\x01896634AA1000\x00\x00\x00\x00', b'\x01896634A88000\x00\x00\x00\x00', b'\x01896634A89000\x00\x00\x00\x00', diff --git a/selfdrive/car/vin.py b/selfdrive/car/vin.py index fba0c54eba..cf1c25e851 100755 --- a/selfdrive/car/vin.py +++ b/selfdrive/car/vin.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 import re -import traceback import cereal.messaging as messaging +from panda.python.uds import get_rx_addr_for_tx_addr, FUNCTIONAL_ADDRS from selfdrive.car.isotp_parallel_query import IsoTpParallelQuery from selfdrive.car.fw_query_definitions import StdQueries from system.swaglog import cloudlog @@ -16,29 +16,48 @@ def is_valid_vin(vin: str): def get_vin(logcan, sendcan, bus, timeout=0.1, retry=5, debug=False): - addrs = [0x7e0, 0x7e2, 0x18da10f1, 0x18da0ef1] # engine, VMCU, 29-bit engine, PGM-FI + addrs = list(range(0x7e0, 0x7e8)) + list(range(0x18DA00F1, 0x18DB00F1, 0x100)) # addrs to process/wait for + valid_vin_addrs = [0x7e0, 0x7e2, 0x18da10f1, 0x18da0ef1] # engine, VMCU, 29-bit engine, PGM-FI for i in range(retry): for request, response in ((StdQueries.UDS_VIN_REQUEST, StdQueries.UDS_VIN_RESPONSE), (StdQueries.OBD_VIN_REQUEST, StdQueries.OBD_VIN_RESPONSE)): try: - query = IsoTpParallelQuery(sendcan, logcan, bus, addrs, [request, ], [response, ], debug=debug) - for (addr, rx_addr), vin in query.get_data(timeout).items(): + query = IsoTpParallelQuery(sendcan, logcan, bus, addrs, [request, ], [response, ], functional_addrs=FUNCTIONAL_ADDRS, debug=debug) + results = query.get_data(timeout) - # Honda Bosch response starts with a length, trim to correct length - if vin.startswith(b'\x11'): - vin = vin[1:18] + for addr in valid_vin_addrs: + vin = results.get((addr, None)) + if vin is not None: + # Ford pads with null bytes + if len(vin) == 24: + vin = re.sub(b'\x00*$', b'', vin) - return addr[0], rx_addr, vin.decode() - print(f"vin query retry ({i+1}) ...") + # Honda Bosch response starts with a length, trim to correct length + if vin.startswith(b'\x11'): + vin = vin[1:18] + + return get_rx_addr_for_tx_addr(addr), vin.decode() + + cloudlog.error(f"vin query retry ({i+1}) ...") except Exception: - cloudlog.warning(f"VIN query exception: {traceback.format_exc()}") + cloudlog.exception("VIN query exception") - return 0, 0, VIN_UNKNOWN + return 0, VIN_UNKNOWN if __name__ == "__main__": + import argparse import time + + parser = argparse.ArgumentParser(description='Get VIN of the car') + parser.add_argument('--debug', action='store_true') + parser.add_argument('--bus', type=int, default=1) + parser.add_argument('--timeout', type=float, default=0.1) + parser.add_argument('--retry', type=int, default=5) + args = parser.parse_args() + sendcan = messaging.pub_sock('sendcan') logcan = messaging.sub_sock('can') time.sleep(1) - addr, vin_rx_addr, vin = get_vin(logcan, sendcan, 1, debug=False) - print(f'TX: {hex(addr)}, RX: {hex(vin_rx_addr)}, VIN: {vin}') + + vin_rx_addr, vin = get_vin(logcan, sendcan, args.bus, args.timeout, args.retry, debug=args.debug) + print(f'RX: {hex(vin_rx_addr)}, VIN: {vin}') diff --git a/selfdrive/car/volkswagen/carcontroller.py b/selfdrive/car/volkswagen/carcontroller.py index 816933f2f0..fff5548671 100644 --- a/selfdrive/car/volkswagen/carcontroller.py +++ b/selfdrive/car/volkswagen/carcontroller.py @@ -76,7 +76,7 @@ class CarController: stopping = actuators.longControlState == LongCtrlState.stopping starting = actuators.longControlState == LongCtrlState.starting can_sends.extend(self.CCS.create_acc_accel_control(self.packer_pt, CANBUS.pt, CS.acc_type, CC.longActive, accel, - acc_control, stopping, starting, CS.out.cruiseState.standstill)) + acc_control, stopping, starting, CS.esp_hold_confirmation)) # **** HUD Controls ***************************************************** # diff --git a/selfdrive/car/volkswagen/carstate.py b/selfdrive/car/volkswagen/carstate.py index cf4a252b65..5dc4543c0d 100644 --- a/selfdrive/car/volkswagen/carstate.py +++ b/selfdrive/car/volkswagen/carstate.py @@ -110,20 +110,21 @@ class CarState(CarStateBase): ret.cruiseState.available = True ret.cruiseState.enabled = False elif pt_cp.vl["TSK_06"]["TSK_Status"] in (3, 4, 5): - # ACC okay and enabled, currently regulating speed (3) or driver accel override (4) or overrun coast-down (5) + # ACC okay and enabled, currently regulating speed (3) or driver accel override (4) or brake only (5) ret.cruiseState.available = True ret.cruiseState.enabled = True else: # ACC okay but disabled (1), or a radar visibility or other fault/disruption (6 or 7) ret.cruiseState.available = False ret.cruiseState.enabled = False - ret.cruiseState.standstill = bool(pt_cp.vl["ESP_21"]["ESP_Haltebestaetigung"]) + self.esp_hold_confirmation = bool(pt_cp.vl["ESP_21"]["ESP_Haltebestaetigung"]) + ret.cruiseState.standstill = self.CP.pcmCruise and self.esp_hold_confirmation ret.accFaulted = pt_cp.vl["TSK_06"]["TSK_Status"] in (6, 7) # Update ACC setpoint. When the setpoint is zero or there's an error, the # radar sends a set-speed of ~90.69 m/s / 203mph. if self.CP.pcmCruise: - ret.cruiseState.speed = ext_cp.vl["ACC_02"]["ACC_Wunschgeschw"] * CV.KPH_TO_MS + ret.cruiseState.speed = ext_cp.vl["ACC_02"]["ACC_Wunschgeschw_02"] * CV.KPH_TO_MS if ret.cruiseState.speed > 90: ret.cruiseState.speed = 0 @@ -170,7 +171,7 @@ class CarState(CarStateBase): ret.gas = pt_cp.vl["Motor_3"]["Fahrpedal_Rohsignal"] / 100.0 ret.gasPressed = ret.gas > 0 ret.brake = pt_cp.vl["Bremse_5"]["Bremsdruck"] / 250.0 # FIXME: this is pressure in Bar, not sure what OP expects - ret.brakePressed = bool(pt_cp.vl["Motor_2"]["Bremstestschalter"]) + ret.brakePressed = bool(pt_cp.vl["Motor_2"]["Bremslichtschalter"]) ret.parkingBrake = bool(pt_cp.vl["Kombi_1"]["Bremsinfo"]) # Update gear and/or clutch position data. @@ -478,7 +479,7 @@ class CarState(CarStateBase): class MqbExtraSignals: # Additional signal and message lists for optional or bus-portable controllers fwd_radar_signals = [ - ("ACC_Wunschgeschw", "ACC_02"), # ACC set speed + ("ACC_Wunschgeschw_02", "ACC_02"), # ACC set speed ("AWV2_Freigabe", "ACC_10"), # FCW brake jerk release ("ANB_Teilbremsung_Freigabe", "ACC_10"), # AEB partial braking release ("ANB_Zielbremsung_Freigabe", "ACC_10"), # AEB target braking release diff --git a/selfdrive/car/volkswagen/interface.py b/selfdrive/car/volkswagen/interface.py index 821eef44c7..870f3ab163 100644 --- a/selfdrive/car/volkswagen/interface.py +++ b/selfdrive/car/volkswagen/interface.py @@ -6,6 +6,7 @@ from selfdrive.car import STD_CARGO_KG, scale_rot_inertia, scale_tire_stiffness, from selfdrive.car.interfaces import CarInterfaceBase from selfdrive.car.volkswagen.values import CAR, PQ_CARS, CANBUS, NetworkLocation, TransmissionType, GearShifter +ButtonType = car.CarState.ButtonEvent.Type EventName = car.CarEvent.EventName @@ -127,6 +128,13 @@ class CarInterface(CarInterfaceBase): ret.mass = 1230 + STD_CARGO_KG ret.wheelbase = 2.55 + elif candidate == CAR.SHARAN_MK2: + ret.mass = 1639 + STD_CARGO_KG + ret.wheelbase = 2.92 + ret.minEnableSpeed = 30 * CV.KPH_TO_MS + ret.minSteerSpeed = 50 * CV.KPH_TO_MS + ret.steerActuatorDelay = 0.2 + elif candidate == CAR.TAOS_MK1: ret.mass = 1498 + STD_CARGO_KG ret.wheelbase = 2.69 @@ -211,7 +219,8 @@ class CarInterface(CarInterfaceBase): ret = self.CS.update(self.cp, self.cp_cam, self.cp_ext, self.CP.transmissionType) events = self.create_common_events(ret, extra_gears=[GearShifter.eco, GearShifter.sport, GearShifter.manumatic], - pcm_enable=not self.CS.CP.openpilotLongitudinalControl) + pcm_enable=not self.CS.CP.openpilotLongitudinalControl, + enable_buttons=(ButtonType.setCruise, ButtonType.resumeCruise)) # Low speed steer alert hysteresis logic if self.CP.minSteerSpeed > 0. and ret.vEgo < (self.CP.minSteerSpeed + 1.): diff --git a/selfdrive/car/volkswagen/pqcan.py b/selfdrive/car/volkswagen/pqcan.py index 30f3fcf62d..0bcbf6abb3 100644 --- a/selfdrive/car/volkswagen/pqcan.py +++ b/selfdrive/car/volkswagen/pqcan.py @@ -59,7 +59,7 @@ def acc_hud_status_value(main_switch_on, acc_faulted, long_active): return hud_status -def create_acc_accel_control(packer, bus, acc_type, enabled, accel, acc_control, stopping, starting, standstill): +def create_acc_accel_control(packer, bus, acc_type, enabled, accel, acc_control, stopping, starting, esp_hold): commands = [] values = { diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index bdd1b5089d..8c679c7406 100755 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -116,6 +116,7 @@ class CAR: PASSAT_MK8 = "VOLKSWAGEN PASSAT 8TH GEN" # Chassis 3G, Mk8 VW Passat and variants PASSAT_NMS = "VOLKSWAGEN PASSAT NMS" # Chassis A3, North America/China/Mideast NMS Passat, incl. facelift POLO_MK6 = "VOLKSWAGEN POLO 6TH GEN" # Chassis AW, Mk6 VW Polo + SHARAN_MK2 = "VOLKSWAGEN SHARAN 2ND GEN" # Chassis 7N, Mk2 Volkswagen Sharan and SEAT Alhambra TAOS_MK1 = "VOLKSWAGEN TAOS 1ST GEN" # Chassis B2, Mk1 VW Taos and Tharu TCROSS_MK1 = "VOLKSWAGEN T-CROSS 1ST GEN" # Chassis C1, Mk1 VW T-Cross SWB and LWB variants TIGUAN_MK2 = "VOLKSWAGEN TIGUAN 2ND GEN" # Chassis AD/BW, Mk2 VW Tiguan and variants @@ -135,7 +136,7 @@ class CAR: SKODA_OCTAVIA_MK3 = "SKODA OCTAVIA 3RD GEN" # Chassis NE, Mk3 Skoda Octavia and variants -PQ_CARS = {CAR.PASSAT_NMS} +PQ_CARS = {CAR.PASSAT_NMS, CAR.SHARAN_MK2} DBC: Dict[str, Dict[str, str]] = defaultdict(lambda: dbc_dict("vw_mqb_2010", None)) @@ -206,6 +207,10 @@ CAR_INFO: Dict[str, Union[VWCarInfo, List[VWCarInfo]]] = { VWCarInfo("Volkswagen Polo 2020-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), VWCarInfo("Volkswagen Polo GTI 2020-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), ], + CAR.SHARAN_MK2: [ + VWCarInfo("Volkswagen Sharan 2018-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + VWCarInfo("SEAT Alhambra 2018-20", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + ], CAR.TAOS_MK1: VWCarInfo("Volkswagen Taos 2022", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), CAR.TCROSS_MK1: VWCarInfo("Volkswagen T-Cross 2021", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), CAR.TIGUAN_MK2: VWCarInfo("Volkswagen Tiguan 2019-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), @@ -236,6 +241,7 @@ CAR_INFO: Dict[str, Union[VWCarInfo, List[VWCarInfo]]] = { ], } + # All supported cars should return FW from the engine, srs, eps, and fwdRadar. Cars # with a manual trans won't return transmission firmware, but all other cars will. # @@ -625,6 +631,18 @@ FW_VERSIONS = { b'\xf1\x872Q0907572R \xf1\x890372', ], }, + CAR.SHARAN_MK2: { + # TODO: Sharan Mk2 EPS and DQ250 auto trans both require KWP2000 support for fingerprinting + (Ecu.engine, 0x7e0, None): [ + b'\xf1\x8704L906016HE\xf1\x894635', + ], + (Ecu.srs, 0x715, None): [ + b'\xf1\x877N0959655D \xf1\x890016\xf1\x82\x0801100705----10--', + ], + (Ecu.fwdRadar, 0x757, None): [ + b'\xf1\x877N0907572C \xf1\x890211\xf1\x82\x0153', + ], + }, CAR.TAOS_MK1: { (Ecu.engine, 0x7e0, None): [ b'\xf1\x8704E906027NJ\xf1\x891445', diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 5639a1f6c7..8abb1a02a6 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -143,6 +143,12 @@ class Controls: put_nonblocking("CarParamsCache", cp_bytes) put_nonblocking("CarParamsPersistent", cp_bytes) + # cleanup old params + if not self.CP.experimentalLongitudinalAvailable: + params.remove("ExperimentalLongitudinalEnabled") + if not self.CP.openpilotLongitudinalControl: + params.remove("EndToEndLong") + self.CC = car.CarControl.new_message() self.CS_prev = car.CarState.new_message() self.AM = AlertManager() @@ -313,7 +319,7 @@ class Controls: else: safety_mismatch = pandaState.safetyModel not in IGNORED_SAFETY_MODES - if safety_mismatch or self.mismatch_counter >= 200: + if safety_mismatch or pandaState.safetyRxChecksInvalid or self.mismatch_counter >= 200: self.events.add(EventName.controlsMismatch) if log.PandaState.FaultType.relayMalfunction in pandaState.faults: @@ -595,7 +601,7 @@ class Controls: CC.enabled = self.enabled # Check which actuators can be enabled CC.latActive = self.active and not CS.steerFaultTemporary and not CS.steerFaultPermanent and \ - CS.vEgo > self.CP.minSteerSpeed and not CS.standstill + CS.vEgo > self.CP.minSteerSpeed and not CS.standstill CC.longActive = self.active and not self.events.any(ET.OVERRIDE_LONGITUDINAL) and self.CP.openpilotLongitudinalControl actuators = CC.actuators @@ -699,6 +705,7 @@ class Controls: if len(angular_rate_value) > 2: CC.angularVelocity = angular_rate_value + 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]: CC.cruiseControl.cancel = True @@ -718,7 +725,7 @@ class Controls: recent_blinker = (self.sm.frame - self.last_blinker_frame) * DT_CTRL < 5.0 # 5s blinker cooldown ldw_allowed = self.is_ldw_enabled and CS.vEgo > LDW_MIN_SPEED and not recent_blinker \ - and not CC.latActive and self.sm['liveCalibration'].calStatus == Calibration.CALIBRATED + and not CC.latActive and self.sm['liveCalibration'].calStatus == Calibration.CALIBRATED model_v2 = self.sm['modelV2'] desire_prediction = model_v2.meta.desirePrediction diff --git a/selfdrive/controls/lib/drive_helpers.py b/selfdrive/controls/lib/drive_helpers.py index ffa8373834..f81cd0c40c 100644 --- a/selfdrive/controls/lib/drive_helpers.py +++ b/selfdrive/controls/lib/drive_helpers.py @@ -33,12 +33,6 @@ CRUISE_INTERVAL_SIGN = { } -class MPC_COST_LAT: - PATH = 1.0 - HEADING = 1.0 - STEER_RATE = 1.0 - - def apply_deadzone(error, deadzone): if error > deadzone: error -= deadzone @@ -125,10 +119,10 @@ def get_lag_adjusted_curvature(CP, v_ego, psis, curvatures, curvature_rates): desired_curvature_rate = curvature_rates[0] max_curvature_rate = MAX_LATERAL_JERK / (v_ego**2) # inexact calculation, check https://github.com/commaai/openpilot/pull/24755 safe_desired_curvature_rate = clip(desired_curvature_rate, - -max_curvature_rate, - max_curvature_rate) + -max_curvature_rate, + max_curvature_rate) safe_desired_curvature = clip(desired_curvature, - current_curvature_desired - max_curvature_rate * DT_MDL, - current_curvature_desired + max_curvature_rate * DT_MDL) + current_curvature_desired - max_curvature_rate * DT_MDL, + current_curvature_desired + max_curvature_rate * DT_MDL) return safe_desired_curvature, safe_desired_curvature_rate diff --git a/selfdrive/controls/lib/events.py b/selfdrive/controls/lib/events.py index 91f1748ecb..5bfe89b31c 100644 --- a/selfdrive/controls/lib/events.py +++ b/selfdrive/controls/lib/events.py @@ -811,6 +811,10 @@ EVENTS: Dict[int, Dict[str, Union[Alert, AlertCallbackType]]] = { ET.NO_ENTRY: NoEntryAlert("Cruise Faulted"), }, + EventName.gmAccFaultedTemp: { + ET.NO_ENTRY: NoEntryAlert("Cruise Temporarily Faulted"), + }, + EventName.controlsMismatch: { ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Controls Mismatch"), ET.NO_ENTRY: NoEntryAlert("Controls Mismatch"), diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index fa1bb480f1..7d656b55a9 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -17,6 +17,8 @@ from selfdrive.controls.lib.vehicle_model import ACCELERATION_DUE_TO_GRAVITY # friction in the steering wheel that needs to be overcome to # move it at all, this is compensated for too. +LOW_SPEED_FACTOR = 100 + class LatControlTorque(LatControl): def __init__(self, CP, CI): @@ -55,13 +57,15 @@ class LatControlTorque(LatControl): actual_lateral_accel = actual_curvature * CS.vEgo ** 2 lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2 - low_speed_factor = interp(CS.vEgo, [0, 10, 20], [500, 500, 200]) - setpoint = desired_lateral_accel + low_speed_factor * desired_curvature - measurement = actual_lateral_accel + low_speed_factor * actual_curvature + setpoint = desired_lateral_accel + LOW_SPEED_FACTOR * desired_curvature + measurement = actual_lateral_accel + LOW_SPEED_FACTOR * actual_curvature error = setpoint - measurement gravity_adjusted_lateral_accel = desired_lateral_accel - params.roll * ACCELERATION_DUE_TO_GRAVITY - pid_log.error = self.torque_from_lateral_accel(error, self.torque_params, error, lateral_accel_deadzone, friction_compensation=False) - ff = self.torque_from_lateral_accel(gravity_adjusted_lateral_accel, self.torque_params, error, lateral_accel_deadzone, friction_compensation=True) + pid_log.error = self.torque_from_lateral_accel(error, self.torque_params, error, + lateral_accel_deadzone, friction_compensation=False) + ff = self.torque_from_lateral_accel(gravity_adjusted_lateral_accel, self.torque_params, + desired_lateral_accel - actual_lateral_accel, + lateral_accel_deadzone, friction_compensation=True) freeze_integrator = steer_limited or CS.steeringPressed or CS.vEgo < 5 output_torque = self.pid.update(pid_log.error, diff --git a/selfdrive/controls/lib/lateral_mpc_lib/lat_mpc.py b/selfdrive/controls/lib/lateral_mpc_lib/lat_mpc.py index c0e7358160..9607532ace 100755 --- a/selfdrive/controls/lib/lateral_mpc_lib/lat_mpc.py +++ b/selfdrive/controls/lib/lateral_mpc_lib/lat_mpc.py @@ -5,7 +5,6 @@ import numpy as np from casadi import SX, vertcat, sin, cos from common.realtime import sec_since_boot -from selfdrive.controls.lib.drive_helpers import LAT_MPC_N as N from selfdrive.modeld.constants import T_IDXS if __name__ == '__main__': # generating code @@ -18,6 +17,10 @@ EXPORT_DIR = os.path.join(LAT_MPC_DIR, "c_generated_code") JSON_FILE = os.path.join(LAT_MPC_DIR, "acados_ocp_lat.json") X_DIM = 4 P_DIM = 2 +N = 16 +COST_E_DIM = 3 +COST_DIM = COST_E_DIM + 2 +SPEED_OFFSET = 10.0 MODEL_NAME = 'lat' ACADOS_SOLVER_TYPE = 'SQP_RTI' @@ -29,8 +32,8 @@ def gen_lat_model(): x_ego = SX.sym('x_ego') y_ego = SX.sym('y_ego') psi_ego = SX.sym('psi_ego') - curv_ego = SX.sym('curv_ego') - model.x = vertcat(x_ego, y_ego, psi_ego, curv_ego) + psi_rate_ego = SX.sym('psi_rate_ego') + model.x = vertcat(x_ego, y_ego, psi_ego, psi_rate_ego) # parameters v_ego = SX.sym('v_ego') @@ -38,22 +41,22 @@ def gen_lat_model(): model.p = vertcat(v_ego, rotation_radius) # controls - curv_rate = SX.sym('curv_rate') - model.u = vertcat(curv_rate) + psi_accel_ego = SX.sym('psi_accel_ego') + model.u = vertcat(psi_accel_ego) # xdot x_ego_dot = SX.sym('x_ego_dot') y_ego_dot = SX.sym('y_ego_dot') psi_ego_dot = SX.sym('psi_ego_dot') - curv_ego_dot = SX.sym('curv_ego_dot') + psi_rate_ego_dot = SX.sym('psi_rate_ego_dot') - model.xdot = vertcat(x_ego_dot, y_ego_dot, psi_ego_dot, curv_ego_dot) + model.xdot = vertcat(x_ego_dot, y_ego_dot, psi_ego_dot, psi_rate_ego_dot) # dynamics model - f_expl = vertcat(v_ego * cos(psi_ego) - rotation_radius * sin(psi_ego) * (v_ego * curv_ego), - v_ego * sin(psi_ego) + rotation_radius * cos(psi_ego) * (v_ego * curv_ego), - v_ego * curv_ego, - curv_rate) + f_expl = vertcat(v_ego * cos(psi_ego) - rotation_radius * sin(psi_ego) * psi_rate_ego, + v_ego * sin(psi_ego) + rotation_radius * cos(psi_ego) * psi_rate_ego, + psi_rate_ego, + psi_accel_ego) model.f_impl_expr = model.xdot - f_expl model.f_expl_expr = f_expl return model @@ -72,26 +75,35 @@ def gen_lat_ocp(): ocp.cost.cost_type = 'NONLINEAR_LS' ocp.cost.cost_type_e = 'NONLINEAR_LS' - Q = np.diag([0.0, 0.0]) - QR = np.diag([0.0, 0.0, 0.0]) + Q = np.diag(np.zeros(COST_E_DIM)) + QR = np.diag(np.zeros(COST_DIM)) ocp.cost.W = QR ocp.cost.W_e = Q - y_ego, psi_ego = ocp.model.x[1], ocp.model.x[2] - curv_rate = ocp.model.u[0] + y_ego, psi_ego, psi_rate_ego = ocp.model.x[1], ocp.model.x[2], ocp.model.x[3] + psi_rate_ego_dot = ocp.model.u[0] v_ego = ocp.model.p[0] ocp.parameter_values = np.zeros((P_DIM, )) - ocp.cost.yref = np.zeros((3, )) - ocp.cost.yref_e = np.zeros((2, )) - # TODO hacky weights to keep behavior the same + ocp.cost.yref = np.zeros((COST_DIM, )) + ocp.cost.yref_e = np.zeros((COST_E_DIM, )) + # Add offset to smooth out low speed control + # TODO unclear if this right solution long term + v_ego_offset = v_ego + SPEED_OFFSET + # TODO there are two costs on psi_rate_ego_dot, one + # is correlated to jerk the other to steering wheel movement + # the steering wheel movement cost is added to prevent excessive + # wheel movements ocp.model.cost_y_expr = vertcat(y_ego, - ((v_ego +5.0) * psi_ego), - ((v_ego + 5.0) * 4.0 * curv_rate)) + v_ego_offset * psi_ego, + v_ego_offset * psi_rate_ego, + v_ego_offset * psi_rate_ego_dot, + psi_rate_ego_dot / (v_ego + 0.1)) ocp.model.cost_y_expr_e = vertcat(y_ego, - ((v_ego +5.0) * psi_ego)) + v_ego_offset * psi_ego, + v_ego_offset * psi_rate_ego) # set constraints ocp.constraints.constr_type = 'BGH' @@ -124,10 +136,10 @@ class LateralMpc(): def reset(self, x0=np.zeros(X_DIM)): self.x_sol = np.zeros((N+1, X_DIM)) self.u_sol = np.zeros((N, 1)) - self.yref = np.zeros((N+1, 3)) + self.yref = np.zeros((N+1, COST_DIM)) for i in range(N): self.solver.cost_set(i, "yref", self.yref[i]) - self.solver.cost_set(N, "yref", self.yref[N][:2]) + self.solver.cost_set(N, "yref", self.yref[N][:COST_E_DIM]) # Somehow needed for stable init for i in range(N+1): @@ -140,14 +152,17 @@ class LateralMpc(): self.solve_time = 0.0 self.cost = 0 - def set_weights(self, path_weight, heading_weight, steer_rate_weight): - W = np.asfortranarray(np.diag([path_weight, heading_weight, steer_rate_weight])) + def set_weights(self, path_weight, heading_weight, + lat_accel_weight, lat_jerk_weight, + steering_rate_weight): + W = np.asfortranarray(np.diag([path_weight, heading_weight, + lat_accel_weight, lat_jerk_weight, + steering_rate_weight])) for i in range(N): self.solver.cost_set(i, 'W', W) - #TODO hacky weights to keep behavior the same - self.solver.cost_set(N, 'W', (3/20.)*W[:2,:2]) + self.solver.cost_set(N, 'W', W[:COST_E_DIM,:COST_E_DIM]) - def run(self, x0, p, y_pts, heading_pts, curv_rate_pts): + def run(self, x0, p, y_pts, heading_pts, yaw_rate_pts): x0_cp = np.copy(x0) p_cp = np.copy(p) self.solver.constraints_set(0, "lbx", x0_cp) @@ -155,13 +170,13 @@ class LateralMpc(): self.yref[:,0] = y_pts v_ego = p_cp[0] # rotation_radius = p_cp[1] - self.yref[:,1] = heading_pts*(v_ego+5.0) - self.yref[:,2] = curv_rate_pts * (v_ego+5.0) * 4.0 + self.yref[:,1] = heading_pts * (v_ego + SPEED_OFFSET) + self.yref[:,2] = yaw_rate_pts * (v_ego + SPEED_OFFSET) for i in range(N): self.solver.cost_set(i, "yref", self.yref[i]) self.solver.set(i, "p", p_cp) self.solver.set(N, "p", p_cp) - self.solver.cost_set(N, "yref", self.yref[N][:2]) + self.solver.cost_set(N, "yref", self.yref[N][:COST_E_DIM]) t = sec_since_boot() self.solution_status = self.solver.solve() diff --git a/selfdrive/controls/lib/lateral_planner.py b/selfdrive/controls/lib/lateral_planner.py index 3470754bc6..9fbfd4a11f 100644 --- a/selfdrive/controls/lib/lateral_planner.py +++ b/selfdrive/controls/lib/lateral_planner.py @@ -3,7 +3,8 @@ from common.realtime import sec_since_boot, DT_MDL from common.numpy_fast import interp from system.swaglog import cloudlog from selfdrive.controls.lib.lateral_mpc_lib.lat_mpc import LateralMpc -from selfdrive.controls.lib.drive_helpers import CONTROL_N, MPC_COST_LAT, LAT_MPC_N +from selfdrive.controls.lib.lateral_mpc_lib.lat_mpc import N as LAT_MPC_N +from selfdrive.controls.lib.drive_helpers import CONTROL_N from selfdrive.controls.lib.desire_helper import DesireHelper import cereal.messaging as messaging from cereal import log @@ -11,6 +12,20 @@ from cereal import log TRAJECTORY_SIZE = 33 CAMERA_OFFSET = 0.04 + +PATH_COST = 1.0 +LATERAL_MOTION_COST = 0.11 +LATERAL_ACCEL_COST = 0.0 +LATERAL_JERK_COST = 0.05 +# Extreme steering rate is unpleasant, even +# when it does not cause bad jerk. +# TODO this cost should be lowered when low +# speed lateral control is stable on all cars +STEERING_RATE_COST = 800.0 + +MIN_SPEED = .3 + + class LateralPlanner: def __init__(self, CP): self.DH = DesireHelper() @@ -22,9 +37,8 @@ class LateralPlanner: self.solution_invalid_cnt = 0 self.path_xyz = np.zeros((TRAJECTORY_SIZE, 3)) - self.path_xyz_stds = np.ones((TRAJECTORY_SIZE, 3)) self.plan_yaw = np.zeros((TRAJECTORY_SIZE,)) - self.plan_curv_rate = np.zeros((TRAJECTORY_SIZE,)) + self.plan_yaw_rate = np.zeros((TRAJECTORY_SIZE,)) self.t_idxs = np.arange(TRAJECTORY_SIZE) self.y_pts = np.zeros(TRAJECTORY_SIZE) @@ -36,7 +50,8 @@ class LateralPlanner: self.lat_mpc.reset(x0=self.x0) def update(self, sm): - v_ego = sm['carState'].vEgo + # clip speed , lateral planning is not possible at 0 speed + self.v_ego = max(MIN_SPEED, sm['carState'].vEgo) measured_curvature = sm['controlsState'].curvature # Parse model predictions @@ -45,8 +60,7 @@ class LateralPlanner: self.path_xyz = np.column_stack([md.position.x, md.position.y, md.position.z]) self.t_idxs = np.array(md.position.t) self.plan_yaw = np.array(md.orientation.z) - if len(md.position.xStd) == TRAJECTORY_SIZE: - self.path_xyz_stds = np.column_stack([md.position.xStd, md.position.yStd, md.position.zStd]) + self.plan_yaw_rate = np.array(md.orientationRate.z) # Lane change logic desire_state = md.meta.desireState @@ -57,29 +71,29 @@ class LateralPlanner: self.DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) d_path_xyz = self.path_xyz - # Heading cost is useful at low speed, otherwise end of plan can be off-heading - heading_cost = interp(v_ego, [5.0, 10.0], [MPC_COST_LAT.HEADING, 0.15]) - self.lat_mpc.set_weights(MPC_COST_LAT.PATH, heading_cost, MPC_COST_LAT.STEER_RATE) + self.lat_mpc.set_weights(PATH_COST, LATERAL_MOTION_COST, + LATERAL_ACCEL_COST, LATERAL_JERK_COST, + STEERING_RATE_COST) - y_pts = np.interp(v_ego * self.t_idxs[:LAT_MPC_N + 1], np.linalg.norm(d_path_xyz, axis=1), d_path_xyz[:, 1]) - heading_pts = np.interp(v_ego * self.t_idxs[:LAT_MPC_N + 1], np.linalg.norm(self.path_xyz, axis=1), self.plan_yaw) - curv_rate_pts = np.interp(v_ego * self.t_idxs[:LAT_MPC_N + 1], np.linalg.norm(self.path_xyz, axis=1), self.plan_curv_rate) + y_pts = np.interp(self.v_ego * self.t_idxs[:LAT_MPC_N + 1], np.linalg.norm(d_path_xyz, axis=1), d_path_xyz[:, 1]) + heading_pts = np.interp(self.v_ego * self.t_idxs[:LAT_MPC_N + 1], np.linalg.norm(self.path_xyz, axis=1), self.plan_yaw) + yaw_rate_pts = np.interp(self.v_ego * self.t_idxs[:LAT_MPC_N + 1], np.linalg.norm(self.path_xyz, axis=1), self.plan_yaw_rate) self.y_pts = y_pts assert len(y_pts) == LAT_MPC_N + 1 assert len(heading_pts) == LAT_MPC_N + 1 - assert len(curv_rate_pts) == LAT_MPC_N + 1 - lateral_factor = max(0, self.factor1 - (self.factor2 * v_ego**2)) - p = np.array([v_ego, lateral_factor]) + assert len(yaw_rate_pts) == LAT_MPC_N + 1 + lateral_factor = max(0, self.factor1 - (self.factor2 * self.v_ego**2)) + p = np.array([self.v_ego, lateral_factor]) self.lat_mpc.run(self.x0, p, y_pts, heading_pts, - curv_rate_pts) - # init state for next - # mpc.u_sol is the desired curvature rate given x0 curv state. - # with x0[3] = measured_curvature, this would be the actual desired rate. - # instead, interpolate x_sol so that x0[3] is the desired curvature for lat_control. + yaw_rate_pts) + # init state for next iteration + # mpc.u_sol is the desired second derivative of psi given x0 curv state. + # with x0[3] = measured_yaw_rate, this would be the actual desired yaw rate. + # instead, interpolate x_sol so that x0[3] is the desired yaw rate for lat_control. self.x0[3] = interp(DT_MDL, self.t_idxs[:LAT_MPC_N + 1], self.lat_mpc.x_sol[:, 3]) # Check for infeasible MPC solution @@ -87,7 +101,7 @@ class LateralPlanner: t = sec_since_boot() if mpc_nans or self.lat_mpc.solution_status != 0: self.reset_mpc() - self.x0[3] = measured_curvature + self.x0[3] = measured_curvature * self.v_ego if t > self.last_cloudlog_t + 5.0: self.last_cloudlog_t = t cloudlog.warning("Lateral mpc - nan: True") @@ -106,8 +120,9 @@ class LateralPlanner: lateralPlan.modelMonoTime = sm.logMonoTime['modelV2'] lateralPlan.dPathPoints = self.y_pts.tolist() lateralPlan.psis = self.lat_mpc.x_sol[0:CONTROL_N, 2].tolist() - lateralPlan.curvatures = self.lat_mpc.x_sol[0:CONTROL_N, 3].tolist() - lateralPlan.curvatureRates = [float(x) for x in self.lat_mpc.u_sol[0:CONTROL_N - 1]] + [0.0] + + lateralPlan.curvatures = (self.lat_mpc.x_sol[0:CONTROL_N, 3]/self.v_ego).tolist() + lateralPlan.curvatureRates = [float(x/self.v_ego) for x in self.lat_mpc.u_sol[0:CONTROL_N - 1]] + [0.0] lateralPlan.mpcSolutionValid = bool(plan_solution_valid) lateralPlan.solverExecutionTime = self.lat_mpc.solve_time diff --git a/selfdrive/controls/lib/longcontrol.py b/selfdrive/controls/lib/longcontrol.py index f72995d414..db5bf4d3e6 100644 --- a/selfdrive/controls/lib/longcontrol.py +++ b/selfdrive/controls/lib/longcontrol.py @@ -7,9 +7,6 @@ from selfdrive.modeld.constants import T_IDXS LongCtrlState = car.CarControl.Actuators.LongControlState -# As per ISO 15622:2018 for all speeds -ACCEL_MIN_ISO = -3.5 # m/s^2 -ACCEL_MAX_ISO = 2.0 # m/s^2 def long_control_state_trans(CP, active, long_control_state, v_ego, v_target, v_target_1sec, brake_pressed, cruise_standstill): @@ -50,10 +47,6 @@ def long_control_state_trans(CP, active, long_control_state, v_ego, v_target, elif started_condition: long_control_state = LongCtrlState.pid - - - - return long_control_state diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py index ea9b0683a4..2aab4b71af 100644 --- a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py +++ b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @@ -253,11 +253,8 @@ class LongitudinalMpc: cost_weights = [X_EGO_OBSTACLE_COST, X_EGO_COST, V_EGO_COST, A_EGO_COST, a_change_cost, J_EGO_COST] constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST] elif self.mode == 'blended': - cost_weights = [0., 0.2, 0.25, 1.0, 0.0, 1.0] + cost_weights = [0., 0.1, 0.2, 5.0, 0.0, 1.0] constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, 50.0] - elif self.mode == 'e2e': - cost_weights = [0., 0.2, 0.25, 1., 0.0, .1] - constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, 0.0] else: raise NotImplementedError(f'Planner mode {self.mode} not recognized in planner cost set') self.set_cost_weights(cost_weights, constraint_cost_weights) @@ -320,7 +317,7 @@ class LongitudinalMpc: # Update in ACC mode or ACC/e2e blend if self.mode == 'acc': - self.params[:,0] = MIN_ACCEL if self.status else self.cruise_min_a + self.params[:,0] = MIN_ACCEL self.params[:,1] = self.cruise_max_a self.params[:,5] = LEAD_DANGER_FACTOR @@ -341,17 +338,18 @@ class LongitudinalMpc: elif self.mode == 'blended': self.params[:,0] = MIN_ACCEL self.params[:,1] = MAX_ACCEL + self.params[:,5] = 1.0 x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle]) - cruise_target = T_IDXS * v_cruise + x[0] + cruise_target = T_IDXS * np.clip(v_cruise, v_ego - 2.0, 1e3) + x[0] xforward = ((v[1:] + v[:-1]) / 2) * (T_IDXS[1:] - T_IDXS[:-1]) x = np.cumsum(np.insert(xforward, 0, x[0])) x_and_cruise = np.column_stack([x, cruise_target]) x = np.min(x_and_cruise, axis=1) - + self.source = 'e2e' if x_and_cruise[0,0] < x_and_cruise[0,1] else 'cruise' else: @@ -385,29 +383,6 @@ class LongitudinalMpc: (lead_1_obstacle[0] - lead_0_obstacle[0]): self.source = 'lead1' - - - def update_with_xva(self, x, v, a): - self.params[:,0] = -10. - self.params[:,1] = 10. - self.params[:,2] = 1e5 - self.params[:,4] = T_FOLLOW - self.params[:,5] = LEAD_DANGER_FACTOR - - # v, and a are in local frame, but x is wrt the x[0] position - # In >90degree turns, x goes to 0 (and may even be -ve) - # So, we use integral(v) + x[0] to obtain the forward-distance - xforward = ((v[1:] + v[:-1]) / 2) * (T_IDXS[1:] - T_IDXS[:-1]) - x = np.cumsum(np.insert(xforward, 0, x[0])) - self.yref[:,1] = x - self.yref[:,2] = v - self.yref[:,3] = a - for i in range(N): - self.solver.cost_set(i, "yref", self.yref[i]) - self.solver.cost_set(N, "yref", self.yref[N][:COST_E_DIM]) - self.params[:,3] = np.copy(self.prev_a) - self.run() - def run(self): # t0 = sec_since_boot() # reset = 0 diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index b9b16e34fc..018136e6f2 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import math import numpy as np -from common.numpy_fast import interp +from common.numpy_fast import clip, interp import cereal.messaging as messaging from common.conversions import Conversions as CV @@ -18,8 +18,8 @@ from system.swaglog import cloudlog LON_MPC_STEP = 0.2 # first step is 0.2s AWARENESS_DECEL = -0.2 # car smoothly decel at .2m/s^2 when user is distracted A_CRUISE_MIN = -1.2 -A_CRUISE_MAX_VALS = [1.2, 1.2, 0.8, 0.6] -A_CRUISE_MAX_BP = [0., 15., 25., 40.] +A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6] +A_CRUISE_MAX_BP = [0., 10.0, 25., 40.] # Lookup table for turns _A_TOTAL_MAX_V = [1.7, 3.2] @@ -58,7 +58,6 @@ class LongitudinalPlanner: self.a_desired = init_a self.v_desired_filter = FirstOrderFilter(init_v, 2.0, DT_MDL) - self.t_uniform = np.arange(0.0, T_IDXS_MPC[-1] + 0.5, 0.5) self.v_desired_trajectory = np.zeros(CONTROL_N) self.a_desired_trajectory = np.zeros(CONTROL_N) @@ -76,10 +75,7 @@ class LongitudinalPlanner: x = np.interp(T_IDXS_MPC, T_IDXS, model_msg.position.x) v = np.interp(T_IDXS_MPC, T_IDXS, model_msg.velocity.x) a = np.interp(T_IDXS_MPC, T_IDXS, model_msg.acceleration.x) - # Uniform interp so gradient is less noisy - a_sparse = np.interp(self.t_uniform, T_IDXS, model_msg.acceleration.x) - j_sparse = np.gradient(a_sparse, self.t_uniform) - j = np.interp(T_IDXS_MPC, self.t_uniform, j_sparse) + j = np.zeros(len(T_IDXS_MPC)) else: x = np.zeros(len(T_IDXS_MPC)) v = np.zeros(len(T_IDXS_MPC)) @@ -87,8 +83,8 @@ class LongitudinalPlanner: j = np.zeros(len(T_IDXS_MPC)) return x, v, a, j - def update(self, sm): - if self.param_read_counter % 50 == 0: + def update(self, sm, read=True): + if self.param_read_counter % 50 == 0 and read: self.read_param() self.param_read_counter += 1 @@ -106,15 +102,17 @@ class LongitudinalPlanner: # No change cost when user is controlling the speed, or when standstill prev_accel_constraint = not (reset_state or sm['carState'].standstill) + accel_limits = [A_CRUISE_MIN, get_max_accel(v_ego)] + accel_limits_turns = limit_accel_in_turns(v_ego, sm['carState'].steeringAngleDeg, accel_limits, self.CP) + if reset_state: self.v_desired_filter.x = v_ego - self.a_desired = 0.0 + # Clip aEgo to cruise limits to prevent large accelerations when becoming active + self.a_desired = clip(sm['carState'].aEgo, accel_limits[0], accel_limits[1]) # Prevent divergence, smooth in current v_ego self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego)) - accel_limits = [A_CRUISE_MIN, get_max_accel(v_ego)] - accel_limits_turns = limit_accel_in_turns(v_ego, sm['carState'].steeringAngleDeg, accel_limits, self.CP) if force_slow_decel: # if required so, force a smooth deceleration accel_limits_turns[1] = min(accel_limits_turns[1], AWARENESS_DECEL) diff --git a/selfdrive/controls/lib/radar_helpers.py b/selfdrive/controls/lib/radar_helpers.py index 0e2b96668f..bf788190cd 100644 --- a/selfdrive/controls/lib/radar_helpers.py +++ b/selfdrive/controls/lib/radar_helpers.py @@ -2,8 +2,7 @@ from common.numpy_fast import mean from common.kalman.simple_kalman import KF1D -# the longer lead decels, the more likely it will keep decelerating -# TODO is this a good default? +# Default lead acceleration decay set to 50% at 1s _LEAD_ACCEL_TAU = 1.5 # radar tracks diff --git a/selfdrive/controls/tests/test_lateral_mpc.py b/selfdrive/controls/tests/test_lateral_mpc.py index 4864dbdc06..df5154b2b4 100644 --- a/selfdrive/controls/tests/test_lateral_mpc.py +++ b/selfdrive/controls/tests/test_lateral_mpc.py @@ -1,7 +1,8 @@ import unittest import numpy as np from selfdrive.controls.lib.lateral_mpc_lib.lat_mpc import LateralMpc -from selfdrive.controls.lib.drive_helpers import LAT_MPC_N, CAR_ROTATION_RADIUS +from selfdrive.controls.lib.drive_helpers import CAR_ROTATION_RADIUS +from selfdrive.controls.lib.lateral_mpc_lib.lat_mpc import N as LAT_MPC_N def run_mpc(lat_mpc=None, v_ref=30., x_init=0., y_init=0., psi_init=0., curvature_init=0., @@ -9,7 +10,7 @@ def run_mpc(lat_mpc=None, v_ref=30., x_init=0., y_init=0., psi_init=0., curvatur if lat_mpc is None: lat_mpc = LateralMpc() - lat_mpc.set_weights(1., 1., 1.) + lat_mpc.set_weights(1., .1, 0.0, .05, 800) y_pts = poly_shift * np.ones(LAT_MPC_N + 1) heading_pts = np.zeros(LAT_MPC_N + 1) @@ -76,9 +77,9 @@ class TestLateralMpc(unittest.TestCase): def test_switch_convergence(self): lat_mpc = LateralMpc() - sol = run_mpc(lat_mpc=lat_mpc, poly_shift=30.0, v_ref=7.0) + sol = run_mpc(lat_mpc=lat_mpc, poly_shift=3.0, v_ref=7.0) right_psi_deg = np.degrees(sol[:,2]) - sol = run_mpc(lat_mpc=lat_mpc, poly_shift=-30.0, v_ref=7.0) + sol = run_mpc(lat_mpc=lat_mpc, poly_shift=-3.0, v_ref=7.0) left_psi_deg = np.degrees(sol[:,2]) np.testing.assert_almost_equal(right_psi_deg, -left_psi_deg, decimal=3) diff --git a/selfdrive/debug/check_freq.py b/selfdrive/debug/check_freq.py index b6f3c91bd0..6436abb4f1 100755 --- a/selfdrive/debug/check_freq.py +++ b/selfdrive/debug/check_freq.py @@ -2,7 +2,7 @@ import argparse import numpy as np from collections import defaultdict, deque -from typing import DefaultDict, Deque +from typing import DefaultDict, Deque, MutableSequence from common.realtime import sec_since_boot import cereal.messaging as messaging @@ -19,7 +19,7 @@ if __name__ == "__main__": socket_names = args.socket sockets = {} - rcv_times: DefaultDict[str, Deque[float]] = 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 = sec_since_boot() diff --git a/selfdrive/debug/check_timings.py b/selfdrive/debug/check_timings.py index 69304f97b5..083e084ca7 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, Deque +from typing import DefaultDict, 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, Deque[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/sensor_data_to_hist.py b/selfdrive/debug/sensor_data_to_hist.py index 3c30b3c17c..ceed4b0ec3 100755 --- a/selfdrive/debug/sensor_data_to_hist.py +++ b/selfdrive/debug/sensor_data_to_hist.py @@ -7,6 +7,7 @@ get interrupts in a 2kHz rate. import argparse import sys +import numpy as np from collections import defaultdict from tools.lib.logreader import LogReader @@ -23,28 +24,22 @@ def parseEvents(log_reader): lsm_data = defaultdict(list) for m in log_reader: - # only sensorEvents - if m.which() != 'sensorEvents': + if m.which() not in ['accelerometer', 'gyroscope']: continue - for se in m.sensorEvents: - # convert data to dictionary - d = se.to_dict() + d = getattr(m, m.which()).to_dict() - if d["timestamp"] == 0: - continue # empty event? + if d["source"] == SRC_BMX and "acceleration" in d: + bmx_data["accel"].append(d["timestamp"] / 1e9) - if d["source"] == SRC_BMX and "acceleration" in d: - bmx_data["accel"].append(d["timestamp"] / 1e9) + if d["source"] == SRC_BMX and "gyroUncalibrated" in d: + bmx_data["gyro"].append(d["timestamp"] / 1e9) - if d["source"] == SRC_BMX and "gyroUncalibrated" in d: - bmx_data["gyro"].append(d["timestamp"] / 1e9) + if d["source"] == SRC_LSM and "acceleration" in d: + lsm_data["accel"].append(d["timestamp"] / 1e9) - if d["source"] == SRC_LSM and "acceleration" in d: - lsm_data["accel"].append(d["timestamp"] / 1e9) - - if d["source"] == SRC_LSM and "gyroUncalibrated" in d: - lsm_data["gyro"].append(d["timestamp"] / 1e9) + if d["source"] == SRC_LSM and "gyroUncalibrated" in d: + lsm_data["gyro"].append(d["timestamp"] / 1e9) return bmx_data, lsm_data @@ -54,11 +49,7 @@ def cleanData(data): return [], [] data.sort() - prev = data[0] - diffs = [] - for v in data[1:]: - diffs.append(v - prev) - prev = v + diffs = np.diff(data) return data, diffs @@ -118,4 +109,3 @@ if __name__ == "__main__": print("check plot...") plt.show() - diff --git a/selfdrive/debug/vw_mqb_config.py b/selfdrive/debug/vw_mqb_config.py index c1068bf067..8c4dbc55ee 100755 --- a/selfdrive/debug/vw_mqb_config.py +++ b/selfdrive/debug/vw_mqb_config.py @@ -70,8 +70,8 @@ if __name__ == "__main__": coding_variant, current_coding_array, coding_byte, coding_bit = None, None, 0, 0 coding_length = len(current_coding) - # EV_SteerAssisMQB covers the majority of MQB racks (EPS_MQB_ZFLS) - if odx_file == "EV_SteerAssisMQB\x00": + # EV_SteerAssisMQB/MNB cover the majority of MQB racks (EPS_MQB_ZFLS) + if odx_file in ("EV_SteerAssisMQB\x00", "EV_SteerAssisMNB\x00"): coding_variant = "ZF" coding_byte = 0 coding_bit = 4 @@ -111,7 +111,7 @@ if __name__ == "__main__": if args.action in ["enable", "disable"]: print("\nAttempting configuration update") - assert(coding_variant in ("ZF", "APA")) + assert(coding_variant in ("ZF", "APA")) # ZF EPS config coding length can be anywhere from 1 to 4 bytes, but the # bit we care about is always in the same place in the first byte if args.action == "enable": diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index c7f2d2ceac..2769f394c5 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -16,7 +16,7 @@ from common.params import Params, put_nonblocking from laika import AstroDog from laika.constants import SECS_IN_HR, SECS_IN_MIN from laika.downloader import DownloadFailed -from laika.ephemeris import Ephemeris, EphemerisType, convert_ublox_ephem +from laika.ephemeris import Ephemeris, EphemerisType, convert_ublox_ephem, parse_qcom_ephem from laika.gps_time import GPSTime from laika.helpers import ConstellationId from laika.raw_gnss import GNSSMeasurement, correct_measurements, process_measurements, read_raw_ublox, read_raw_qcom @@ -61,6 +61,7 @@ class Laikad: self.last_pos_fix = [] self.last_pos_residual = [] self.last_pos_fix_t = None + self.gps_week = None self.use_qcom = use_qcom def load_cache(self): @@ -107,11 +108,11 @@ class Laikad: return self.last_pos_fix def is_good_report(self, gnss_msg): - if gnss_msg.which == 'drMeasurementReport' and self.use_qcom: + if gnss_msg.which() == 'drMeasurementReport' and self.use_qcom: constellation_id = ConstellationId.from_qcom_source(gnss_msg.drMeasurementReport.source) # TODO support GLONASS return constellation_id in [ConstellationId.GPS, ConstellationId.SBAS] - elif gnss_msg.which == 'measurementReport' and not self.use_qcom: + elif gnss_msg.which() == 'measurementReport' and not self.use_qcom: return True else: return False @@ -129,9 +130,28 @@ class Laikad: new_meas = read_raw_ublox(report) return week, tow, new_meas + def is_ephemeris(self, gnss_msg): + if self.use_qcom: + return gnss_msg.which() == 'drSvPoly' + else: + return gnss_msg.which() == 'ephemeris' + + def read_ephemeris(self, gnss_msg): + # TODO this only works on GLONASS + if self.use_qcom: + # TODO this is not robust to gps week rollover + if self.gps_week is None: + return + ephem = parse_qcom_ephem(gnss_msg.drSvPoly, self.gps_week) + else: + ephem = convert_ublox_ephem(gnss_msg.ephemeris) + self.astro_dog.add_navs({ephem.prn: [ephem]}) + self.cache_ephemeris(t=ephem.epoch) + def process_gnss_msg(self, gnss_msg, gnss_mono_time: int, block=False): if self.is_good_report(gnss_msg): week, tow, new_meas = self.read_report(gnss_msg) + self.gps_week = week t = gnss_mono_time * 1e-9 if week > 0: @@ -172,12 +192,10 @@ class Laikad: "correctedMeasurements": meas_msgs } return dat - # TODO this only works on GLONASS, qcom needs live ephemeris parsing too - elif gnss_msg.which == 'ephemeris': - ephem = convert_ublox_ephem(gnss_msg.ephemeris) - self.astro_dog.add_navs({ephem.prn: [ephem]}) - self.cache_ephemeris(t=ephem.epoch) - #elif gnss_msg.which == 'ionoData': + elif self.is_ephemeris(gnss_msg): + self.read_ephemeris(gnss_msg) + + #elif gnss_msg.which() == 'ionoData': # todo add this. Needed to better correct messages offline. First fix ublox_msg.cc to sent them. def update_localizer(self, est_pos, t: float, measurements: List[GNSSMeasurement]): @@ -265,9 +283,11 @@ def create_measurement_msg(meas: GNSSMeasurement): c.satVel = meas.sat_vel.tolist() ephem = meas.sat_ephemeris assert ephem is not None + week, time_of_week = -1, -1 if ephem.eph_type == EphemerisType.NAV: source_type = EphemerisSourceType.nav - week, time_of_week = -1, -1 + elif ephem.eph_type == EphemerisType.QCOM_POLY: + source_type = EphemerisSourceType.qcom else: assert ephem.file_epoch is not None week = ephem.file_epoch.week @@ -325,10 +345,11 @@ class EphemerisSourceType(IntEnum): nav = 0 nasaUltraRapid = 1 glonassIacUltraRapid = 2 + qcom = 3 def main(sm=None, pm=None): - use_qcom = os.path.isfile("/persist/comma/use-quectel-rawgps") + use_qcom = not Params().get_bool("UbloxAvailable", block=True) if use_qcom: raw_gnss_socket = "qcomGnss" else: @@ -348,6 +369,17 @@ def main(sm=None, pm=None): if sm.updated[raw_gnss_socket]: gnss_msg = sm[raw_gnss_socket] + + # TODO: Understand and use remaining unknown constellations + if gnss_msg.which() == "drMeasurementReport": + if getattr(gnss_msg, gnss_msg.which()).source not in ['glonass', 'gps', 'beidou', 'sbas']: + continue + + if getattr(gnss_msg, gnss_msg.which()).gpsWeek > np.iinfo(np.int16).max: + # gpsWeek 65535 is received rarely from quectel, this cannot be + # passed to GnssMeasurements's gpsWeek (Int16) + continue + msg = laikad.process_gnss_msg(gnss_msg, sm.logMonoTime[raw_gnss_socket], block=replay) if msg is not None: pm.send('gnssMeasurements', msg) diff --git a/selfdrive/locationd/locationd.cc b/selfdrive/locationd/locationd.cc index 2fb3e0081d..e156af5d64 100755 --- a/selfdrive/locationd/locationd.cc +++ b/selfdrive/locationd/locationd.cc @@ -20,6 +20,11 @@ const double VALID_POS_STD = 50.0; // m const double MAX_RESET_TRACKER = 5.0; const double SANE_GPS_UNCERTAINTY = 1500.0; // m +// TODO: GPS sensor time offsets are empirically calculated +// They should be replaced with synced time from a real clock +const double GPS_LOCATION_SENSOR_TIME_OFFSET = 0.630; // s +const double GPS_LOCATION_EXTERNAL_SENSOR_TIME_OFFSET = 0.095; // s + static VectorXd floatlist2vector(const capnp::List::Reader& floatlist) { VectorXd res(floatlist.size()); for (int i = 0; i < floatlist.size(); i++) { @@ -195,51 +200,48 @@ VectorXd Localizer::get_stdev() { return this->kf->get_P().diagonal().array().sqrt(); } -void Localizer::handle_sensors(double current_time, const capnp::List::Reader& log) { +void Localizer::handle_sensor(double current_time, const cereal::SensorEventData::Reader& log) { // TODO does not yet account for double sensor readings in the log - for (int i = 0; i < log.size(); i++) { - const cereal::SensorEventData::Reader& sensor_reading = log[i]; - // Ignore empty readings (e.g. in case the magnetometer had no data ready) - if (sensor_reading.getTimestamp() == 0) { - continue; - } + // Ignore empty readings (e.g. in case the magnetometer had no data ready) + if (log.getTimestamp() == 0) { + return; + } - double sensor_time = 1e-9 * sensor_reading.getTimestamp(); + double sensor_time = 1e-9 * log.getTimestamp(); - // sensor time and log time should be close - if (std::abs(current_time - sensor_time) > 0.1) { - LOGE("Sensor reading ignored, sensor timestamp more than 100ms off from log time"); - return; - } + // sensor time and log time should be close + if (std::abs(current_time - sensor_time) > 0.1) { + LOGE("Sensor reading ignored, sensor timestamp more than 100ms off from log time"); + return; + } - // TODO: handle messages from two IMUs at the same time - if (sensor_reading.getSource() == cereal::SensorEventData::SensorSource::BMX055) { - continue; - } + // TODO: handle messages from two IMUs at the same time + if (log.getSource() == cereal::SensorEventData::SensorSource::BMX055) { + return; + } - // Gyro Uncalibrated - if (sensor_reading.getSensor() == SENSOR_GYRO_UNCALIBRATED && sensor_reading.getType() == SENSOR_TYPE_GYROSCOPE_UNCALIBRATED) { - auto v = sensor_reading.getGyroUncalibrated().getV(); - auto meas = Vector3d(-v[2], -v[1], -v[0]); - if (meas.norm() < ROTATION_SANITY_CHECK) { - this->kf->predict_and_observe(sensor_time, OBSERVATION_PHONE_GYRO, { meas }); - } + // Gyro Uncalibrated + if (log.getSensor() == SENSOR_GYRO_UNCALIBRATED && log.getType() == SENSOR_TYPE_GYROSCOPE_UNCALIBRATED) { + auto v = log.getGyroUncalibrated().getV(); + auto meas = Vector3d(-v[2], -v[1], -v[0]); + if (meas.norm() < ROTATION_SANITY_CHECK) { + this->kf->predict_and_observe(sensor_time, OBSERVATION_PHONE_GYRO, { meas }); } + } - // Accelerometer - if (sensor_reading.getSensor() == SENSOR_ACCELEROMETER && sensor_reading.getType() == SENSOR_TYPE_ACCELEROMETER) { - auto v = sensor_reading.getAcceleration().getV(); + // Accelerometer + if (log.getSensor() == SENSOR_ACCELEROMETER && log.getType() == SENSOR_TYPE_ACCELEROMETER) { + auto v = log.getAcceleration().getV(); - // TODO: reduce false positives and re-enable this check - // check if device fell, estimate 10 for g - // 40m/s**2 is a good filter for falling detection, no false positives in 20k minutes of driving - //this->device_fell |= (floatlist2vector(v) - Vector3d(10.0, 0.0, 0.0)).norm() > 40.0; + // TODO: reduce false positives and re-enable this check + // check if device fell, estimate 10 for g + // 40m/s**2 is a good filter for falling detection, no false positives in 20k minutes of driving + // this->device_fell |= (floatlist2vector(v) - Vector3d(10.0, 0.0, 0.0)).norm() > 40.0; - auto meas = Vector3d(-v[2], -v[1], -v[0]); - if (meas.norm() < ACCEL_SANITY_CHECK) { - this->kf->predict_and_observe(sensor_time, OBSERVATION_PHONE_ACCEL, { meas }); - } + auto meas = Vector3d(-v[2], -v[1], -v[0]); + if (meas.norm() < ACCEL_SANITY_CHECK) { + this->kf->predict_and_observe(sensor_time, OBSERVATION_PHONE_ACCEL, { meas }); } } } @@ -260,7 +262,7 @@ void Localizer::input_fake_gps_observations(double current_time) { this->kf->predict_and_observe(current_time, OBSERVATION_ECEF_VEL, { ecef_vel }, { ecef_vel_R }); } -void Localizer::handle_gps(double current_time, const cereal::GpsLocationData::Reader& log) { +void Localizer::handle_gps(double current_time, const cereal::GpsLocationData::Reader& log, const double sensor_time_offset) { // ignore the message if the fix is invalid bool gps_invalid_flag = (log.getFlags() % 2 == 0); bool gps_unreasonable = (Vector2d(log.getAccuracy(), log.getVerticalAccuracy()).norm() >= SANE_GPS_UNCERTAINTY); @@ -268,13 +270,15 @@ void Localizer::handle_gps(double current_time, const cereal::GpsLocationData::R bool gps_lat_lng_alt_insane = ((std::abs(log.getLatitude()) > 90) || (std::abs(log.getLongitude()) > 180) || (std::abs(log.getAltitude()) > ALTITUDE_SANITY_CHECK)); bool gps_vel_insane = (floatlist2vector(log.getVNED()).norm() > TRANS_SANITY_CHECK); - if (gps_invalid_flag || gps_unreasonable || gps_accuracy_insane || gps_lat_lng_alt_insane || gps_vel_insane){ + if (gps_invalid_flag || gps_unreasonable || gps_accuracy_insane || gps_lat_lng_alt_insane || gps_vel_insane) { this->determine_gps_mode(current_time); return; } + + double sensor_time = current_time - sensor_time_offset; // Process message - this->last_gps_fix = current_time; + this->last_gps_fix = sensor_time; this->gps_mode = true; Geodetic geodetic = { log.getLatitude(), log.getLongitude(), log.getAltitude() }; this->converter = std::make_unique(geodetic); @@ -303,14 +307,14 @@ void Localizer::handle_gps(double current_time, const cereal::GpsLocationData::R if (ecef_vel.norm() > 5.0 && orientation_error.norm() > 1.0) { LOGE("Locationd vs ubloxLocation orientation difference too large, kalman reset"); this->reset_kalman(NAN, initial_pose_ecef_quat, ecef_pos, ecef_vel, ecef_pos_R, ecef_vel_R); - this->kf->predict_and_observe(current_time, OBSERVATION_ECEF_ORIENTATION_FROM_GPS, { initial_pose_ecef_quat }); + this->kf->predict_and_observe(sensor_time, OBSERVATION_ECEF_ORIENTATION_FROM_GPS, { initial_pose_ecef_quat }); } else if (gps_est_error > 100.0) { LOGE("Locationd vs ubloxLocation position difference too large, kalman reset"); this->reset_kalman(NAN, initial_pose_ecef_quat, ecef_pos, ecef_vel, ecef_pos_R, ecef_vel_R); } - this->kf->predict_and_observe(current_time, OBSERVATION_ECEF_POS, { ecef_pos }, { ecef_pos_R }); - this->kf->predict_and_observe(current_time, OBSERVATION_ECEF_VEL, { ecef_vel }, { ecef_vel_R }); + this->kf->predict_and_observe(sensor_time, OBSERVATION_ECEF_POS, { ecef_pos }, { ecef_pos_R }); + this->kf->predict_and_observe(sensor_time, OBSERVATION_ECEF_VEL, { ecef_vel }, { ecef_vel_R }); } void Localizer::handle_car_state(double current_time, const cereal::CarState::Reader& log) { @@ -441,12 +445,14 @@ void Localizer::handle_msg_bytes(const char *data, const size_t size) { void Localizer::handle_msg(const cereal::Event::Reader& log) { double t = log.getLogMonoTime() * 1e-9; this->time_check(t); - if (log.isSensorEvents()) { - this->handle_sensors(t, log.getSensorEvents()); + if (log.isAccelerometer()) { + this->handle_sensor(t, log.getAccelerometer()); + } else if (log.isGyroscope()) { + this->handle_sensor(t, log.getGyroscope()); } else if (log.isGpsLocation()) { - this->handle_gps(t, log.getGpsLocation()); + this->handle_gps(t, log.getGpsLocation(), GPS_LOCATION_SENSOR_TIME_OFFSET); } else if (log.isGpsLocationExternal()) { - this->handle_gps(t, log.getGpsLocationExternal()); + this->handle_gps(t, log.getGpsLocationExternal(), GPS_LOCATION_EXTERNAL_SENSOR_TIME_OFFSET); } else if (log.isCarState()) { this->handle_car_state(t, log.getCarState()); } else if (log.isCameraOdometry()) { @@ -493,12 +499,13 @@ void Localizer::determine_gps_mode(double current_time) { int Localizer::locationd_thread() { const char* gps_location_socket; - if (util::file_exists("/persist/comma/use-quectel-rawgps")) { - gps_location_socket = "gpsLocation"; - } else { + if (Params().getBool("UbloxAvailable", true)) { gps_location_socket = "gpsLocationExternal"; + } else { + gps_location_socket = "gpsLocation"; } - const std::initializer_list service_list = {gps_location_socket, "sensorEvents", "cameraOdometry", "liveCalibration", "carState", "carParams"}; + const std::initializer_list service_list = {gps_location_socket, "cameraOdometry", "liveCalibration", + "carState", "carParams", "accelerometer", "gyroscope"}; PubMaster pm({"liveLocationKalman"}); // TODO: remove carParams once we're always sending at 100Hz @@ -521,11 +528,11 @@ int Localizer::locationd_thread() { } // 100Hz publish for notcars, 20Hz for cars - const char* trigger_msg = sm["carParams"].getCarParams().getNotCar() ? "sensorEvents" : "cameraOdometry"; + const char* trigger_msg = sm["carParams"].getCarParams().getNotCar() ? "accelerometer" : "cameraOdometry"; if (sm.updated(trigger_msg)) { bool inputsOK = sm.allAliveAndValid(); - bool sensorsOK = sm.alive("sensorEvents") && sm.valid("sensorEvents"); bool gpsOK = this->isGpsOK(); + bool sensorsOK = sm.allAliveAndValid({"accelerometer", "gyroscope"}); MessageBuilder msg_builder; kj::ArrayPtr bytes = this->get_message_bytes(msg_builder, inputsOK, sensorsOK, gpsOK, filterInitialized); diff --git a/selfdrive/locationd/locationd.h b/selfdrive/locationd/locationd.h index 7c0cb6b7fe..280296b06c 100755 --- a/selfdrive/locationd/locationd.h +++ b/selfdrive/locationd/locationd.h @@ -45,8 +45,8 @@ public: void handle_msg_bytes(const char *data, const size_t size); void handle_msg(const cereal::Event::Reader& log); - void handle_sensors(double current_time, const capnp::List::Reader& log); - void handle_gps(double current_time, const cereal::GpsLocationData::Reader& log); + void handle_sensor(double current_time, const cereal::SensorEventData::Reader& log); + void handle_gps(double current_time, const cereal::GpsLocationData::Reader& log, const double sensor_time_offset); void handle_car_state(double current_time, const cereal::CarState::Reader& log); void handle_cam_odo(double current_time, const cereal::CameraOdometry::Reader& log); void handle_live_calib(double current_time, const cereal::LiveCalibrationData::Reader& log); diff --git a/selfdrive/locationd/models/live_kf.cc b/selfdrive/locationd/models/live_kf.cc index 5ff0f26995..f8c03365e1 100755 --- a/selfdrive/locationd/models/live_kf.cc +++ b/selfdrive/locationd/models/live_kf.cc @@ -44,7 +44,7 @@ LiveKalman::LiveKalman() { // init filter this->filter = std::make_shared(this->name, get_mapmat(this->Q), get_mapvec(this->initial_x), get_mapmat(initial_P), this->dim_state, this->dim_state_err, 0, 0, 0, std::vector(), - std::vector{3}, std::vector(), 0.2); + std::vector{3}, std::vector(), 0.8); } void LiveKalman::init_state(VectorXd& state, VectorXd& covs_diag, double filter_time) { diff --git a/selfdrive/locationd/test/_test_locationd_lib.py b/selfdrive/locationd/test/_test_locationd_lib.py index 8a0ed3ef05..c4a053bbc6 100755 --- a/selfdrive/locationd/test/_test_locationd_lib.py +++ b/selfdrive/locationd/test/_test_locationd_lib.py @@ -51,23 +51,23 @@ void localizer_handle_msg_bytes(Localizer_t localizer, const char *data, size_t @unittest.skip("temporarily disabled due to false positives") def test_device_fell(self): - msg = messaging.new_message('sensorEvents', 1) - msg.sensorEvents[0].sensor = 1 - msg.sensorEvents[0].timestamp = msg.logMonoTime - msg.sensorEvents[0].type = 1 - msg.sensorEvents[0].init('acceleration') - msg.sensorEvents[0].acceleration.v = [10.0, 0.0, 0.0] # zero with gravity + msg = messaging.new_message('accelerometer') + msg.accelerometer.sensor = 1 + msg.accelerometer.timestamp = msg.logMonoTime + msg.accelerometer.type = 1 + msg.accelerometer.init('acceleration') + msg.accelerometer.acceleration.v = [10.0, 0.0, 0.0] # zero with gravity self.localizer_handle_msg(msg) ret = self.localizer_get_msg() self.assertTrue(ret.liveLocationKalman.deviceStable) - msg = messaging.new_message('sensorEvents', 1) - msg.sensorEvents[0].sensor = 1 - msg.sensorEvents[0].timestamp = msg.logMonoTime - msg.sensorEvents[0].type = 1 - msg.sensorEvents[0].init('acceleration') - msg.sensorEvents[0].acceleration.v = [50.1, 0.0, 0.0] # more than 40 m/s**2 + msg = messaging.new_message('accelerometer') + msg.accelerometer.sensor = 1 + msg.accelerometer.timestamp = msg.logMonoTime + msg.accelerometer.type = 1 + msg.accelerometer.init('acceleration') + msg.accelerometer.acceleration.v = [50.1, 0.0, 0.0] # more than 40 m/s**2 self.localizer_handle_msg(msg) ret = self.localizer_get_msg() diff --git a/selfdrive/locationd/test/test_locationd.py b/selfdrive/locationd/test/test_locationd.py index e30331a460..7569530211 100755 --- a/selfdrive/locationd/test/test_locationd.py +++ b/selfdrive/locationd/test/test_locationd.py @@ -14,13 +14,15 @@ from selfdrive.manager.process_config import managed_processes class TestLocationdProc(unittest.TestCase): MAX_WAITS = 1000 - LLD_MSGS = ['gpsLocationExternal', 'cameraOdometry', 'carState', 'sensorEvents', 'liveCalibration'] + LLD_MSGS = ['gpsLocationExternal', 'cameraOdometry', 'carState', 'liveCalibration', + 'accelerometer', 'gyroscope', 'magnetometer'] def setUp(self): random.seed(123489234) self.pm = messaging.PubMaster(self.LLD_MSGS) + Params().put_bool("UbloxAvailable", True) managed_processes['locationd'].prepare() managed_processes['locationd'].start() @@ -51,6 +53,7 @@ class TestLocationdProc(unittest.TestCase): msg.gpsLocationExternal.vNED = [0.0, 0.0, 0.0] msg.gpsLocationExternal.latitude = self.lat msg.gpsLocationExternal.longitude = self.lon + msg.gpsLocationExternal.unixTimestampMillis = t * 1e6 msg.gpsLocationExternal.altitude = self.alt elif name == 'cameraOdometry': msg.cameraOdometry.rot = [0.0, 0.0, 0.0] diff --git a/selfdrive/locationd/torqued.py b/selfdrive/locationd/torqued.py index 9f07008214..66af234590 100755 --- a/selfdrive/locationd/torqued.py +++ b/selfdrive/locationd/torqued.py @@ -12,7 +12,6 @@ from common.realtime import config_realtime_process, DT_MDL from common.filter_simple import FirstOrderFilter from system.swaglog import cloudlog from selfdrive.controls.lib.vehicle_model import ACCELERATION_DUE_TO_GRAVITY -from selfdrive.car.toyota.values import CAR as TOYOTA HISTORY = 5 # secs POINTS_PER_BUCKET = 1500 @@ -33,7 +32,7 @@ MAX_INVALID_THRESHOLD = 10 MIN_ENGAGE_BUFFER = 2 # secs VERSION = 1 # bump this to invalidate old parameter caches -ALLOWED_FINGERPRINTS = [TOYOTA.COROLLA_TSS2, TOYOTA.COROLLA, TOYOTA.COROLLAH_TSS2] +ALLOWED_CARS = ['toyota', 'hyundai'] def slope2rot(slope): @@ -42,7 +41,7 @@ def slope2rot(slope): return np.array([[cos, -sin], [sin, cos]]) -class npqueue: +class NPQueue: def __init__(self, maxlen, rowsize): self.maxlen = maxlen self.arr = np.empty((0, rowsize)) @@ -61,7 +60,7 @@ class npqueue: class PointBuckets: def __init__(self, x_bounds, min_points): self.x_bounds = x_bounds - self.buckets = {bounds: npqueue(maxlen=POINTS_PER_BUCKET, rowsize=3) for bounds in x_bounds} + self.buckets = {bounds: NPQueue(maxlen=POINTS_PER_BUCKET, rowsize=3) for bounds in x_bounds} self.buckets_min_points = {bounds: min_point for bounds, min_point in zip(x_bounds, min_points)} def bucket_lengths(self): @@ -80,7 +79,7 @@ class PointBuckets: break def get_points(self, num_points=None): - points = np.concatenate([x.arr for x in self.buckets.values() if len(x) > 0]) + 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)] @@ -98,7 +97,7 @@ class TorqueEstimator: self.offline_friction = 0.0 self.offline_latAccelFactor = 0.0 self.resets = 0.0 - self.use_params = CP.carFingerprint in ALLOWED_FINGERPRINTS + self.use_params = CP.carName in ALLOWED_CARS if CP.lateralTuning.which() == 'torque': self.offline_friction = CP.lateralTuning.torque.friction @@ -127,12 +126,13 @@ class TorqueEstimator: cache_ltp = log.Event.from_bytes(torque_cache).liveTorqueParameters cache_CP = car.CarParams.from_bytes(params_cache) if self.get_restore_key(cache_CP, cache_ltp.version) == self.get_restore_key(CP, VERSION): - initial_params = { - 'latAccelFactor': cache_ltp.latAccelFactorFiltered, - 'latAccelOffset': cache_ltp.latAccelOffsetFiltered, - 'frictionCoefficient': cache_ltp.frictionCoefficientFiltered, - 'points': cache_ltp.points - } + if cache_ltp.liveValid: + initial_params = { + 'latAccelFactor': cache_ltp.latAccelFactorFiltered, + 'latAccelOffset': cache_ltp.latAccelOffsetFiltered, + 'frictionCoefficient': cache_ltp.frictionCoefficientFiltered + } + initial_params['points'] = cache_ltp.points self.decay = cache_ltp.decay self.filtered_points.load_points(initial_params['points']) cloudlog.info("restored torque params from cache") @@ -224,7 +224,7 @@ class TorqueEstimator: self.update_params({'latAccelFactor': latAccelFactor, 'latAccelOffset': latAccelOffset, 'frictionCoefficient': friction_coeff}) self.invalid_values_tracker = max(0.0, self.invalid_values_tracker - 0.5) else: - cloudlog.exception("live torque params are numerically unstable") + cloudlog.exception("Live torque parameters are outside acceptable bounds.") liveTorqueParameters.liveValid = False self.invalid_values_tracker += 1.0 # Reset when ~10 invalid over 5 secs diff --git a/selfdrive/loggerd/bootlog.cc b/selfdrive/loggerd/bootlog.cc index 6ff052655a..e882e4cf8d 100644 --- a/selfdrive/loggerd/bootlog.cc +++ b/selfdrive/loggerd/bootlog.cc @@ -55,13 +55,11 @@ int main(int argc, char** argv) { bool r = util::create_directories(LOG_ROOT + "/boot/", 0775); assert(r); - RawFile bz_file(path.c_str()); - + RawFile file(path.c_str()); // Write initdata - bz_file.write(logger_build_init_data().asBytes()); - + file.write(logger_build_init_data().asBytes()); // Write bootlog - bz_file.write(build_boot_log().asBytes()); + file.write(build_boot_log().asBytes()); return 0; } diff --git a/selfdrive/loggerd/tests/test_loggerd.py b/selfdrive/loggerd/tests/test_loggerd.py index a2138b0aa6..9c3565d130 100755 --- a/selfdrive/loggerd/tests/test_loggerd.py +++ b/selfdrive/loggerd/tests/test_loggerd.py @@ -110,6 +110,8 @@ class TestLoggerd(unittest.TestCase): self.assertEqual(getattr(initData, initData_key), v) self.assertEqual(logged_params[param_key].decode(), v) + params.put("LaikadEphemeris", "") + def test_rotation(self): os.environ["LOGGERD_TEST"] = "1" Params().put("RecordFront", "1") diff --git a/selfdrive/manager/manager.py b/selfdrive/manager/manager.py index ff2bf4bc89..928507f65b 100755 --- a/selfdrive/manager/manager.py +++ b/selfdrive/manager/manager.py @@ -39,6 +39,7 @@ def manager_init() -> None: default_params: List[Tuple[str, Union[str, bytes]]] = [ ("CompletedTrainingVersion", "0"), ("DisengageOnAccelerator", "1"), + ("GsmMetered", "1"), ("HasAcceptedTerms", "0"), ("LanguageSetting", "main_en"), ("OpenpilotEnabledToggle", "1"), diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index 06efdbb960..3f63fbb959 100644 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -43,6 +43,7 @@ procs = [ PythonProcess("deleter", "selfdrive.loggerd.deleter", offroad=True), PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", enabled=(not PC or WEBCAM), callback=driverview), PythonProcess("laikad", "selfdrive.locationd.laikad"), + PythonProcess("rawgpsd", "selfdrive.sensord.rawgps.rawgpsd", enabled=TICI), PythonProcess("navd", "selfdrive.navd.navd"), PythonProcess("pandad", "selfdrive.boardd.pandad", offroad=True), PythonProcess("paramsd", "selfdrive.locationd.paramsd"), @@ -55,11 +56,9 @@ procs = [ PythonProcess("uploader", "selfdrive.loggerd.uploader", offroad=True), PythonProcess("statsd", "selfdrive.statsd", offroad=True), + # debug procs NativeProcess("bridge", "cereal/messaging", ["./bridge"], onroad=False, callback=notcar), PythonProcess("webjoystick", "tools.joystick.web", onroad=False, callback=notcar), - - # Experimental - PythonProcess("rawgpsd", "selfdrive.sensord.rawgps.rawgpsd", enabled=(TICI and os.path.isfile("/persist/comma/use-quectel-rawgps"))), ] managed_processes = {p.name: p for p in procs} diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 246f8c2941..5c02e2b15f 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -71,9 +71,9 @@ if use_thneed and arch == "larch64" or GetOption('pc_thneed'): fn = File("models/supercombo").abspath if GetOption('pc_thneed'): - cmd = f"cd {Dir('#').abspath}/tinygrad_repo && NATIVE_EXPLOG=1 OPTWG=1 UNSAFE_FLOAT4=1 DEBUGCL=1 python3 openpilot/compile.py {fn}.onnx {fn}.thneed" + cmd = f"cd {Dir('#').abspath}/tinygrad_repo && GPU=1 NATIVE_EXPLOG=1 OPTWG=1 UNSAFE_FLOAT4=1 DEBUGCL=1 python3 openpilot/compile.py {fn}.onnx {fn}.thneed" else: - cmd = f"cd {Dir('#').abspath}/tinygrad_repo && FLOAT16=1 PYOPENCL_NO_CACHE=1 MATMUL=1 NATIVE_EXPLOG=1 OPTWG=1 UNSAFE_FLOAT4=1 DEBUGCL=1 python3 openpilot/compile.py {fn}.onnx {fn}.thneed" + cmd = f"cd {Dir('#').abspath}/tinygrad_repo && FLOAT16=1 MATMUL=1 PYOPENCL_NO_CACHE=1 NATIVE_EXPLOG=1 OPTWG=1 UNSAFE_FLOAT4=1 DEBUGCL=1 python3 openpilot/compile.py {fn}.onnx {fn}.thneed" # is there a better way then listing all of tinygrad? lenv.Command(fn + ".thneed", [fn + ".onnx", diff --git a/selfdrive/modeld/modeld.cc b/selfdrive/modeld/modeld.cc index 653661a3a8..cfc71a0e27 100644 --- a/selfdrive/modeld/modeld.cc +++ b/selfdrive/modeld/modeld.cc @@ -35,7 +35,7 @@ mat3 update_calibration(Eigen::Vector3d device_from_calib_euler, bool wide_camer 0.00000000e+00, 7.31372216e-19, 1.00000000e+00, 2.19780220e-03, 4.11497335e-19, -5.62637363e-01, -6.66298828e-20, 2.19780220e-03, -3.33626374e-01).finished(); - + static const auto view_from_device = (Eigen::Matrix() << 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, diff --git a/selfdrive/modeld/models/driving.cc b/selfdrive/modeld/models/driving.cc index 8d02eb6b2f..3316e6d114 100644 --- a/selfdrive/modeld/models/driving.cc +++ b/selfdrive/modeld/models/driving.cc @@ -41,11 +41,11 @@ void model_init(ModelState* s, cl_device_id device_id, cl_context context) { &s->output[0], NET_OUTPUT_SIZE, USE_GPU_RUNTIME, true, false, context); #ifdef TEMPORAL - s->m->addRecurrent(&s->output[OUTPUT_SIZE], TEMPORAL_SIZE); + s->m->addRecurrent(&s->feature_buffer[0], TEMPORAL_SIZE); #endif #ifdef DESIRE - s->m->addDesire(s->pulse_desire, DESIRE_LEN); + s->m->addDesire(s->pulse_desire, DESIRE_LEN*(HISTORY_BUFFER_LEN+1)); #endif #ifdef TRAFFIC_CONVENTION @@ -56,18 +56,20 @@ void model_init(ModelState* s, cl_device_id device_id, cl_context context) { ModelOutput* model_eval_frame(ModelState* s, VisionBuf* buf, VisionBuf* wbuf, const mat3 &transform, const mat3 &transform_wide, float *desire_in, bool is_rhd, bool prepare_only) { #ifdef DESIRE + std::memmove(&s->pulse_desire[0], &s->pulse_desire[DESIRE_LEN], sizeof(float) * DESIRE_LEN*HISTORY_BUFFER_LEN); if (desire_in != NULL) { for (int i = 1; i < DESIRE_LEN; i++) { // Model decides when action is completed // so desire input is just a pulse triggered on rising edge if (desire_in[i] - s->prev_desire[i] > .99) { - s->pulse_desire[i] = desire_in[i]; + s->pulse_desire[DESIRE_LEN*(HISTORY_BUFFER_LEN-1)+i] = desire_in[i]; } else { - s->pulse_desire[i] = 0.0; + s->pulse_desire[DESIRE_LEN*(HISTORY_BUFFER_LEN-1)+i] = 0.0; } s->prev_desire[i] = desire_in[i]; } } +LOGT("Desire enqueued"); #endif int rhd_idx = is_rhd; @@ -92,6 +94,12 @@ ModelOutput* model_eval_frame(ModelState* s, VisionBuf* buf, VisionBuf* wbuf, s->m->execute(); LOGT("Execution finished"); + #ifdef TEMPORAL + std::memmove(&s->feature_buffer[0], &s->feature_buffer[FEATURE_LEN], sizeof(float) * FEATURE_LEN*(HISTORY_BUFFER_LEN-1)); + std::memcpy(&s->feature_buffer[FEATURE_LEN*(HISTORY_BUFFER_LEN-1)], &s->output[OUTPUT_SIZE], sizeof(float) * FEATURE_LEN); + LOGT("Features enqueued"); + #endif + return (ModelOutput*)&s->output; } diff --git a/selfdrive/modeld/models/driving.h b/selfdrive/modeld/models/driving.h index e2ee812e44..8bb84d0245 100644 --- a/selfdrive/modeld/models/driving.h +++ b/selfdrive/modeld/models/driving.h @@ -16,6 +16,8 @@ #include "selfdrive/modeld/models/commonmodel.h" #include "selfdrive/modeld/runners/run.h" +constexpr int FEATURE_LEN = 128; +constexpr int HISTORY_BUFFER_LEN = 99; constexpr int DESIRE_LEN = 8; constexpr int DESIRE_PRED_LEN = 4; constexpr int TRAFFIC_CONVENTION_LEN = 2; @@ -233,6 +235,11 @@ struct ModelOutputMeta { }; static_assert(sizeof(ModelOutputMeta) == sizeof(ModelOutputDesireProb) + sizeof(float) + (sizeof(ModelOutputDisengageProb)*DISENGAGE_LEN) + (sizeof(ModelOutputBlinkerProb)*BLINKER_LEN) + (sizeof(ModelOutputDesireProb)*DESIRE_PRED_LEN)); +struct ModelOutputFeatures { + std::array feature; +}; +static_assert(sizeof(ModelOutputFeatures) == (sizeof(float)*FEATURE_LEN)); + struct ModelOutput { const ModelOutputPlans plans; const ModelOutputLaneLines lane_lines; @@ -244,22 +251,24 @@ struct ModelOutput { }; constexpr int OUTPUT_SIZE = sizeof(ModelOutput) / sizeof(float); + #ifdef TEMPORAL - constexpr int TEMPORAL_SIZE = 512; + constexpr int TEMPORAL_SIZE = HISTORY_BUFFER_LEN * FEATURE_LEN; #else constexpr int TEMPORAL_SIZE = 0; #endif -constexpr int NET_OUTPUT_SIZE = OUTPUT_SIZE + TEMPORAL_SIZE; +constexpr int NET_OUTPUT_SIZE = OUTPUT_SIZE + FEATURE_LEN; // TODO: convert remaining arrays to std::array and update model runners struct ModelState { ModelFrame *frame = nullptr; ModelFrame *wide_frame = nullptr; + std::array feature_buffer = {}; std::array output = {}; std::unique_ptr m; #ifdef DESIRE float prev_desire[DESIRE_LEN] = {}; - float pulse_desire[DESIRE_LEN] = {}; + float pulse_desire[DESIRE_LEN*(HISTORY_BUFFER_LEN+1)] = {}; #endif #ifdef TRAFFIC_CONVENTION float traffic_convention[TRAFFIC_CONVENTION_LEN] = {}; diff --git a/selfdrive/modeld/models/supercombo.onnx b/selfdrive/modeld/models/supercombo.onnx index 7b11edbe08..59b8883d2a 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:50c7fc8565ac69a4b9a0de122e961326820e78bf13659255a89d0ed04be030d5 -size 95167481 +oid sha256:022a830c39267f378f45204682060c93e3aa304bbd8cfa6b2dfe4fa8f419102d +size 56972617 diff --git a/selfdrive/modeld/runners/onnx_runner.py b/selfdrive/modeld/runners/onnx_runner.py index ac7cc68814..d4a11a7c0b 100755 --- a/selfdrive/modeld/runners/onnx_runner.py +++ b/selfdrive/modeld/runners/onnx_runner.py @@ -9,6 +9,8 @@ os.environ["OMP_WAIT_POLICY"] = "PASSIVE" import onnxruntime as ort # pylint: disable=import-error +ORT_TYPES_TO_NP_TYPES = {'tensor(float16)': np.float16, 'tensor(float)': np.float32, 'tensor(uint8)': np.uint8} + def read(sz, tf8=False): dd = [] gt = 0 @@ -18,7 +20,7 @@ def read(sz, tf8=False): assert(len(st) > 0) dd.append(st) gt += len(st) - r = np.frombuffer(b''.join(dd), dtype=np.uint8 if tf8 else np.float32).astype(np.float32) + r = np.frombuffer(b''.join(dd), dtype=np.uint8 if tf8 else np.float32) if tf8: r = r / 255. return r @@ -29,22 +31,23 @@ def write(d): def run_loop(m, tf8_input=False): ishapes = [[1]+ii.shape[1:] for ii in m.get_inputs()] keys = [x.name for x in m.get_inputs()] + itypes = [ORT_TYPES_TO_NP_TYPES[x.type] for x in m.get_inputs()] # run once to initialize CUDA provider if "CUDAExecutionProvider" in m.get_providers(): - m.run(None, dict(zip(keys, [np.zeros(shp, dtype=np.float32) for shp in ishapes]))) + m.run(None, dict(zip(keys, [np.zeros(shp, dtype=itp) for shp, itp in zip(ishapes, itypes)]))) print("ready to run onnx model", keys, ishapes, file=sys.stderr) while 1: inputs = [] - for k, shp in zip(keys, ishapes): + for k, shp, itp in zip(keys, ishapes, itypes): ts = np.product(shp) #print("reshaping %s with offset %d" % (str(shp), offset), file=sys.stderr) - inputs.append(read(ts, (k=='input_img' and tf8_input)).reshape(shp)) + inputs.append(read(ts, (k=='input_img' and tf8_input)).reshape(shp).astype(itp)) ret = m.run(None, dict(zip(keys, inputs))) #print(ret, file=sys.stderr) for r in ret: - write(r) + write(r.astype(np.float32)) if __name__ == "__main__": diff --git a/selfdrive/modeld/tests/__init__.py b/selfdrive/modeld/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/selfdrive/modeld/test/dmon_lag/repro.cc b/selfdrive/modeld/tests/dmon_lag/repro.cc similarity index 100% rename from selfdrive/modeld/test/dmon_lag/repro.cc rename to selfdrive/modeld/tests/dmon_lag/repro.cc diff --git a/selfdrive/modeld/test/snpe_benchmark/.gitignore b/selfdrive/modeld/tests/snpe_benchmark/.gitignore similarity index 100% rename from selfdrive/modeld/test/snpe_benchmark/.gitignore rename to selfdrive/modeld/tests/snpe_benchmark/.gitignore diff --git a/selfdrive/modeld/test/snpe_benchmark/benchmark.cc b/selfdrive/modeld/tests/snpe_benchmark/benchmark.cc similarity index 99% rename from selfdrive/modeld/test/snpe_benchmark/benchmark.cc rename to selfdrive/modeld/tests/snpe_benchmark/benchmark.cc index 1e2072eea1..021e065d81 100644 --- a/selfdrive/modeld/test/snpe_benchmark/benchmark.cc +++ b/selfdrive/modeld/tests/snpe_benchmark/benchmark.cc @@ -102,6 +102,7 @@ void get_testframe(int index, std::unique_ptr &input) { fread(frame_buffer, length, 1, pFile); // std::cout << *(frame_buffer+length/4-1) << std::endl; std::copy(frame_buffer, frame_buffer+(length/4), input->begin()); + fclose(pFile); } void SaveITensor(const std::string& path, const zdl::DlSystem::ITensor* tensor) diff --git a/selfdrive/modeld/test/snpe_benchmark/benchmark.sh b/selfdrive/modeld/tests/snpe_benchmark/benchmark.sh similarity index 100% rename from selfdrive/modeld/test/snpe_benchmark/benchmark.sh rename to selfdrive/modeld/tests/snpe_benchmark/benchmark.sh diff --git a/selfdrive/modeld/test/test_modeld.py b/selfdrive/modeld/tests/test_modeld.py similarity index 100% rename from selfdrive/modeld/test/test_modeld.py rename to selfdrive/modeld/tests/test_modeld.py diff --git a/selfdrive/modeld/test/tf_test/build.sh b/selfdrive/modeld/tests/tf_test/build.sh similarity index 100% rename from selfdrive/modeld/test/tf_test/build.sh rename to selfdrive/modeld/tests/tf_test/build.sh diff --git a/selfdrive/modeld/test/tf_test/main.cc b/selfdrive/modeld/tests/tf_test/main.cc similarity index 100% rename from selfdrive/modeld/test/tf_test/main.cc rename to selfdrive/modeld/tests/tf_test/main.cc diff --git a/selfdrive/modeld/test/tf_test/pb_loader.py b/selfdrive/modeld/tests/tf_test/pb_loader.py similarity index 100% rename from selfdrive/modeld/test/tf_test/pb_loader.py rename to selfdrive/modeld/tests/tf_test/pb_loader.py diff --git a/selfdrive/modeld/test/timing/benchmark.py b/selfdrive/modeld/tests/timing/benchmark.py similarity index 100% rename from selfdrive/modeld/test/timing/benchmark.py rename to selfdrive/modeld/tests/timing/benchmark.py diff --git a/selfdrive/modeld/thneed/thneed_common.cc b/selfdrive/modeld/thneed/thneed_common.cc index 21170b13a6..ecdf1237e3 100644 --- a/selfdrive/modeld/thneed/thneed_common.cc +++ b/selfdrive/modeld/thneed/thneed_common.cc @@ -12,7 +12,7 @@ map, int> g_args_size; map g_program_source; void Thneed::stop() { - printf("Thneed::stop: recorded %lu commands\n", cmds.size()); + //printf("Thneed::stop: recorded %lu commands\n", cmds.size()); record = false; } diff --git a/selfdrive/navd/SConscript b/selfdrive/navd/SConscript index 4fbe41e80b..b10684eef3 100644 --- a/selfdrive/navd/SConscript +++ b/selfdrive/navd/SConscript @@ -11,6 +11,8 @@ if arch in ['larch64', 'x86_64']: rpath = [Dir(f"#third_party/mapbox-gl-native-qt/{arch}").srcnode().abspath] qt_env["RPATH"] += rpath + style_path = File("style.json").abspath + qt_env['CXXFLAGS'].append(f'-DSTYLE_PATH=\\"{style_path}\\"') qt_libs = ["qt_widgets", "qt_util", "qmapboxgl"] + base_libs nav_src = ["main.cc", "map_renderer.cc"] diff --git a/selfdrive/navd/map_renderer.cc b/selfdrive/navd/map_renderer.cc index d0770cfb48..f85916a4ca 100644 --- a/selfdrive/navd/map_renderer.cc +++ b/selfdrive/navd/map_renderer.cc @@ -1,14 +1,16 @@ #include "selfdrive/navd/map_renderer.h" +#include #include #include #include +#include "common/util.h" #include "common/timing.h" #include "selfdrive/ui/qt/maps/map_helpers.h" -const float ZOOM = 13.5; // Don't go below 13 or features will start to disappear -const int WIDTH = 256; +const float DEFAULT_ZOOM = 13.5; // Don't go below 13 or features will start to disappear +const int WIDTH = 512; const int HEIGHT = WIDTH; const int NUM_VIPC_BUFFERS = 4; @@ -35,9 +37,10 @@ MapRenderer::MapRenderer(const QMapboxGLSettings &settings, bool online) : m_set QOpenGLFramebufferObjectFormat fbo_format; fbo.reset(new QOpenGLFramebufferObject(WIDTH, HEIGHT, fbo_format)); + std::string style = util::read_file(STYLE_PATH); m_map.reset(new QMapboxGL(nullptr, m_settings, fbo->size(), 1)); - m_map->setCoordinateZoom(QMapbox::Coordinate(0, 0), ZOOM); - m_map->setStyleUrl("mapbox://styles/commaai/ckvmksrpd4n0a14pfdo5heqzr"); + m_map->setCoordinateZoom(QMapbox::Coordinate(0, 0), DEFAULT_ZOOM); + m_map->setStyleJson(style.c_str()); m_map->createRenderer(); m_map->resize(fbo->size()); @@ -82,6 +85,15 @@ void MapRenderer::msgUpdate() { } } +void MapRenderer::updateZoom(float zoom) { + if (m_map.isNull()) { + return; + } + + m_map->setZoom(zoom); + update(); +} + void MapRenderer::updatePosition(QMapbox::Coordinate position, float bearing) { if (m_map.isNull()) { return; @@ -185,7 +197,7 @@ void MapRenderer::initLayers() { nav["source"] = "navSource"; m_map->addLayer(nav, "road-intersection"); m_map->setPaintProperty("navLayer", "line-color", QColor("grey")); - m_map->setPaintProperty("navLayer", "line-width", 3); + m_map->setPaintProperty("navLayer", "line-width", 5); m_map->setLayoutProperty("navLayer", "line-cap", "round"); } } @@ -210,6 +222,11 @@ extern "C" { return new MapRenderer(settings, false); } + void map_renderer_update_zoom(MapRenderer *inst, float zoom) { + inst->updateZoom(zoom); + QApplication::processEvents(); + } + void map_renderer_update_position(MapRenderer *inst, float lat, float lon, float bearing) { inst->updatePosition({lat, lon}, bearing); QApplication::processEvents(); diff --git a/selfdrive/navd/map_renderer.h b/selfdrive/navd/map_renderer.h index 855dc91894..921d871632 100644 --- a/selfdrive/navd/map_renderer.h +++ b/selfdrive/navd/map_renderer.h @@ -47,6 +47,7 @@ private: QTimer* timer; public slots: + void updateZoom(float zoom); void updatePosition(QMapbox::Coordinate position, float bearing); void updateRoute(QList coordinates); void msgUpdate(); diff --git a/selfdrive/navd/map_renderer.py b/selfdrive/navd/map_renderer.py index 9000622928..079bb028ce 100755 --- a/selfdrive/navd/map_renderer.py +++ b/selfdrive/navd/map_renderer.py @@ -9,7 +9,7 @@ from cffi import FFI from common.ffi_wrapper import suffix from common.basedir import BASEDIR -HEIGHT = WIDTH = 256 +HEIGHT = WIDTH = 512 def get_ffi(): @@ -18,6 +18,7 @@ def get_ffi(): ffi = FFI() ffi.cdef(""" void* map_renderer_init(char *maps_host, char *token); +void map_renderer_update_zoom(void *inst, float zoom); void map_renderer_update_position(void *inst, float lat, float lon, float bearing); void map_renderer_update_route(void *inst, char *polyline); void map_renderer_update(void *inst); diff --git a/selfdrive/navd/navd.py b/selfdrive/navd/navd.py index 72874b2113..4855b63594 100755 --- a/selfdrive/navd/navd.py +++ b/selfdrive/navd/navd.py @@ -120,6 +120,10 @@ class RouteEngine: cloudlog.warning(f"Calculating route {self.last_position} -> {destination}") self.nav_destination = destination + lang = self.params.get('LanguageSetting', encoding='utf8') + if lang is not None: + lang = lang.replace('main_', '') + params = { 'access_token': self.mapbox_token, 'annotations': 'maxspeed', @@ -128,6 +132,7 @@ class RouteEngine: 'steps': 'true', 'banner_instructions': 'true', 'alternatives': 'false', + 'language': lang, } if self.last_bearing is not None: diff --git a/selfdrive/navd/style.json b/selfdrive/navd/style.json new file mode 100644 index 0000000000..06bb750d1f --- /dev/null +++ b/selfdrive/navd/style.json @@ -0,0 +1 @@ +{"version": 8, "name": "Navigation Model", "metadata": {"mapbox:type": "default", "mapbox:origin": "monochrome-dark-v1", "mapbox:sdk-support": {"android": "10.0.0", "ios": "10.0.0", "js": "2.3.0"}, "mapbox:autocomposite": true, "mapbox:groups": {"Transit, transit-labels": {"name": "Transit, transit-labels", "collapsed": true}, "Administrative boundaries, admin": {"name": "Administrative boundaries, admin", "collapsed": true}, "Transit, bridges": {"name": "Transit, bridges", "collapsed": true}, "Transit, surface": {"name": "Transit, surface", "collapsed": true}, "Road network, bridges": {"name": "Road network, bridges", "collapsed": false}, "Land, water, & sky, water": {"name": "Land, water, & sky, water", "collapsed": true}, "Road network, tunnels": {"name": "Road network, tunnels", "collapsed": false}, "Road network, road-labels": {"name": "Road network, road-labels", "collapsed": true}, "Buildings, built": {"name": "Buildings, built", "collapsed": true}, "Natural features, natural-labels": {"name": "Natural features, natural-labels", "collapsed": true}, "Road network, surface": {"name": "Road network, surface", "collapsed": false}, "Land, water, & sky, built": {"name": "Land, water, & sky, built", "collapsed": true}, "Place labels, place-labels": {"name": "Place labels, place-labels", "collapsed": true}, "Point of interest labels, poi-labels": {"name": "Point of interest labels, poi-labels", "collapsed": true}, "Road network, tunnels-case": {"name": "Road network, tunnels-case", "collapsed": true}, "Transit, built": {"name": "Transit, built", "collapsed": true}, "Road network, surface-icons": {"name": "Road network, surface-icons", "collapsed": false}, "Land, water, & sky, land": {"name": "Land, water, & sky, land", "collapsed": true}}}, "center": [-117.19189443261149, 32.756553679559985], "zoom": 12.932776547838778, "bearing": 0, "pitch": 0.5017568344510897, "sources": {"composite": {"url": "mapbox://mapbox.mapbox-streets-v8", "type": "vector", "maxzoom": 13}}, "sprite": "mapbox://sprites/commaai/ckvmksrpd4n0a14pfdo5heqzr/bkx9h9tjdf3xedbnjvfo5xnbv", "glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf", "layers": [{"id": "land", "type": "background", "layout": {"visibility": "none"}, "paint": {"background-color": "rgb(252, 252, 252)"}, "metadata": {"mapbox:featureComponent": "land-and-water", "mapbox:group": "Land, water, & sky, land"}}, {"minzoom": 5, "layout": {"visibility": "none"}, "metadata": {"mapbox:featureComponent": "land-and-water", "mapbox:group": "Land, water, & sky, land"}, "filter": ["==", ["get", "class"], "national_park"], "type": "fill", "source": "composite", "id": "national-park", "paint": {"fill-color": "rgb(240, 240, 240)", "fill-opacity": ["interpolate", ["linear"], ["zoom"], 5, 0, 6, 0.5, 10, 0.5]}, "source-layer": "landuse_overlay"}, {"minzoom": 5, "layout": {"visibility": "none"}, "metadata": {"mapbox:featureComponent": "land-and-water", "mapbox:group": "Land, water, & sky, land"}, "filter": ["match", ["get", "class"], ["park", "airport", "glacier", "pitch", "sand", "facility"], true, false], "type": "fill", "source": "composite", "id": "landuse", "paint": {"fill-color": "rgb(240, 240, 240)", "fill-opacity": ["interpolate", ["linear"], ["zoom"], 5, 0, 6, ["match", ["get", "class"], "glacier", 0.5, 1]]}, "source-layer": "landuse"}, {"id": "waterway-shadow", "type": "line", "source": "composite", "source-layer": "waterway", "minzoom": 8, "layout": {"line-cap": ["step", ["zoom"], "butt", 11, "round"], "line-join": "round", "visibility": "none"}, "paint": {"line-color": "rgb(204, 204, 204)", "line-width": ["interpolate", ["exponential", 1.3], ["zoom"], 9, ["match", ["get", "class"], ["canal", "river"], 0.1, 0], 20, ["match", ["get", "class"], ["canal", "river"], 8, 3]], "line-translate": ["interpolate", ["exponential", 1.2], ["zoom"], 7, ["literal", [0, 0]], 16, ["literal", [-1, -1]]], "line-translate-anchor": "viewport", "line-opacity": ["interpolate", ["linear"], ["zoom"], 8, 0, 8.5, 1]}, "metadata": {"mapbox:featureComponent": "land-and-water", "mapbox:group": "Land, water, & sky, water"}}, {"id": "water-shadow", "type": "fill", "source": "composite", "source-layer": "water", "layout": {"visibility": "none"}, "paint": {"fill-color": "rgb(204, 204, 204)", "fill-translate": ["interpolate", ["exponential", 1.2], ["zoom"], 7, ["literal", [0, 0]], 16, ["literal", [-1, -1]]], "fill-translate-anchor": "viewport"}, "metadata": {"mapbox:featureComponent": "land-and-water", "mapbox:group": "Land, water, & sky, water"}}, {"id": "waterway", "type": "line", "source": "composite", "source-layer": "waterway", "minzoom": 8, "layout": {"line-cap": ["step", ["zoom"], "butt", 11, "round"], "line-join": "round", "visibility": "none"}, "paint": {"line-color": "rgb(224, 224, 224)", "line-width": ["interpolate", ["exponential", 1.3], ["zoom"], 9, ["match", ["get", "class"], ["canal", "river"], 0.1, 0], 20, ["match", ["get", "class"], ["canal", "river"], 8, 3]], "line-opacity": ["interpolate", ["linear"], ["zoom"], 8, 0, 8.5, 1]}, "metadata": {"mapbox:featureComponent": "land-and-water", "mapbox:group": "Land, water, & sky, water"}}, {"id": "water", "type": "fill", "source": "composite", "source-layer": "water", "layout": {"visibility": "none"}, "paint": {"fill-color": "rgb(224, 224, 224)"}, "metadata": {"mapbox:featureComponent": "land-and-water", "mapbox:group": "Land, water, & sky, water"}}, {"minzoom": 13, "layout": {"visibility": "none"}, "metadata": {"mapbox:featureComponent": "land-and-water", "mapbox:group": "Land, water, & sky, built"}, "filter": ["all", ["==", ["geometry-type"], "Polygon"], ["==", ["get", "class"], "land"]], "type": "fill", "source": "composite", "id": "land-structure-polygon", "paint": {"fill-color": "rgb(252, 252, 252)"}, "source-layer": "structure"}, {"minzoom": 13, "layout": {"line-cap": "round", "visibility": "none"}, "metadata": {"mapbox:featureComponent": "land-and-water", "mapbox:group": "Land, water, & sky, built"}, "filter": ["all", ["==", ["geometry-type"], "LineString"], ["==", ["get", "class"], "land"]], "type": "line", "source": "composite", "id": "land-structure-line", "paint": {"line-width": ["interpolate", ["exponential", 1.99], ["zoom"], 14, 0.75, 20, 40], "line-color": "rgb(252, 252, 252)"}, "source-layer": "structure"}, {"minzoom": 11, "layout": {"visibility": "none"}, "metadata": {"mapbox:featureComponent": "transit", "mapbox:group": "Transit, built"}, "filter": ["all", ["==", ["geometry-type"], "Polygon"], ["match", ["get", "type"], ["runway", "taxiway", "helipad"], true, false]], "type": "fill", "source": "composite", "id": "aeroway-polygon", "paint": {"fill-color": "rgb(255, 255, 255)", "fill-opacity": ["interpolate", ["linear"], ["zoom"], 11, 0, 11.5, 1]}, "source-layer": "aeroway"}, {"minzoom": 9, "layout": {"visibility": "none"}, "metadata": {"mapbox:featureComponent": "transit", "mapbox:group": "Transit, built"}, "filter": ["==", ["geometry-type"], "LineString"], "type": "line", "source": "composite", "id": "aeroway-line", "paint": {"line-color": "rgb(255, 255, 255)", "line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 9, ["match", ["get", "type"], "runway", 1, 0.5], 18, ["match", ["get", "type"], "runway", 80, 20]]}, "source-layer": "aeroway"}, {"minzoom": 13, "layout": {"visibility": "none"}, "metadata": {"mapbox:featureComponent": "buildings", "mapbox:group": "Buildings, built"}, "filter": ["all", ["!=", ["get", "type"], "building:part"], ["==", ["get", "underground"], "false"]], "type": "line", "source": "composite", "id": "building-outline", "paint": {"line-color": "rgb(227, 227, 227)", "line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 15, 0.75, 20, 3], "line-opacity": ["interpolate", ["linear"], ["zoom"], 15, 0, 16, 1]}, "source-layer": "building"}, {"minzoom": 13, "layout": {"visibility": "none"}, "metadata": {"mapbox:featureComponent": "buildings", "mapbox:group": "Buildings, built"}, "filter": ["all", ["!=", ["get", "type"], "building:part"], ["==", ["get", "underground"], "false"]], "type": "fill", "source": "composite", "id": "building", "paint": {"fill-color": ["interpolate", ["linear"], ["zoom"], 15, "rgb(242, 242, 242)", 16, "rgb(242, 242, 242)"], "fill-opacity": ["interpolate", ["linear"], ["zoom"], 15, 0, 16, 1], "fill-outline-color": "rgb(227, 227, 227)"}, "source-layer": "building"}, {"minzoom": 13, "layout": {}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, tunnels-case"}, "filter": ["all", ["==", ["get", "structure"], "tunnel"], ["step", ["zoom"], ["match", ["get", "class"], ["street", "street_limited", "primary_link", "track"], true, false], 1, ["match", ["get", "class"], ["street", "street_limited", "track", "primary_link", "secondary_link", "tertiary_link", "service"], true, false]], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "tunnel-street-minor-low", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, ["match", ["get", "class"], ["street", "street_limited", "primary_link"], 2, "track", 1, 0.5], 18, ["match", ["get", "class"], ["street", "street_limited", "primary_link"], 18, 12]], "line-color": "rgb(235, 235, 235)", "line-opacity": ["step", ["zoom"], 1, 14, 0]}, "source-layer": "road"}, {"minzoom": 13, "layout": {}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, tunnels-case"}, "filter": ["all", ["==", ["get", "structure"], "tunnel"], ["step", ["zoom"], ["match", ["get", "class"], ["street", "street_limited", "primary_link", "track"], true, false], 1, ["match", ["get", "class"], ["street", "street_limited", "track", "primary_link", "secondary_link", "tertiary_link", "service"], true, false]], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "tunnel-street-minor-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.75, 20, 2], "line-color": "rgb(255, 255, 255)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, ["match", ["get", "class"], ["street", "street_limited", "primary_link"], 2, "track", 1, 0.5], 18, ["match", ["get", "class"], ["street", "street_limited", "primary_link"], 18, 12]], "line-opacity": ["step", ["zoom"], 0, 14, 1], "line-dasharray": [3, 3]}, "source-layer": "road"}, {"minzoom": 13, "layout": {}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, tunnels-case"}, "filter": ["all", ["==", ["get", "structure"], "tunnel"], ["match", ["get", "class"], ["primary", "secondary", "tertiary"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "tunnel-primary-secondary-tertiary-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 10, ["match", ["get", "class"], "primary", 1, 0.75], 18, 2], "line-color": "rgb(255, 255, 255)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, ["match", ["get", "class"], "primary", 0.75, 0.1], 18, ["match", ["get", "class"], "primary", 32, 26]], "line-dasharray": [3, 3]}, "source-layer": "road"}, {"minzoom": 13, "layout": {}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, tunnels-case"}, "filter": ["all", ["==", ["get", "structure"], "tunnel"], ["match", ["get", "class"], ["motorway_link", "trunk_link"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "tunnel-major-link-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.75, 20, 2], "line-color": "rgb(255, 255, 255)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, 2, 18, 18], "line-dasharray": [3, 3]}, "source-layer": "road"}, {"minzoom": 13, "layout": {}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, tunnels-case"}, "filter": ["all", ["==", ["get", "structure"], "tunnel"], ["match", ["get", "class"], ["motorway", "trunk"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "tunnel-motorway-trunk-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 10, 1, 18, 2], "line-color": "rgb(255, 255, 255)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, 0.75, 18, 32], "line-dasharray": [3, 3]}, "source-layer": "road"}, {"minzoom": 13, "layout": {}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, tunnels-case"}, "filter": ["all", ["==", ["get", "structure"], "tunnel"], ["==", ["get", "class"], "construction"], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "tunnel-construction", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 14, 2, 18, 18], "line-color": "rgb(235, 235, 235)", "line-dasharray": ["step", ["zoom"], ["literal", [0.4, 0.8]], 15, ["literal", [0.3, 0.6]], 16, ["literal", [0.2, 0.3]], 17, ["literal", [0.2, 0.25]], 18, ["literal", [0.15, 0.15]]]}, "source-layer": "road"}, {"minzoom": 13, "layout": {}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, tunnels"}, "filter": ["all", ["==", ["get", "structure"], "tunnel"], ["match", ["get", "class"], ["motorway_link", "trunk_link"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "tunnel-major-link", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, 2, 18, 18], "line-color": "rgb(235, 235, 235)"}, "source-layer": "road"}, {"minzoom": 13, "layout": {}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, tunnels"}, "filter": ["all", ["==", ["get", "structure"], "tunnel"], ["step", ["zoom"], ["match", ["get", "class"], ["street", "street_limited", "primary_link", "track"], true, false], 1, ["match", ["get", "class"], ["street", "street_limited", "track", "primary_link", "secondary_link", "tertiary_link", "service"], true, false]], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "tunnel-street-minor", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, ["match", ["get", "class"], ["street", "street_limited", "primary_link"], 2, "track", 1, 0.5], 18, ["match", ["get", "class"], ["street", "street_limited", "primary_link"], 18, 12]], "line-color": "rgb(235, 235, 235)", "line-opacity": ["step", ["zoom"], 0, 14, 1]}, "source-layer": "road"}, {"minzoom": 13, "layout": {}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, tunnels"}, "filter": ["all", ["==", ["get", "structure"], "tunnel"], ["match", ["get", "class"], ["primary", "secondary", "tertiary"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "tunnel-primary-secondary-tertiary", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, ["match", ["get", "class"], "primary", 0.75, 0.1], 18, ["match", ["get", "class"], "primary", 32, 26]], "line-color": "rgb(235, 235, 235)"}, "source-layer": "road"}, {"minzoom": 13, "layout": {}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, tunnels"}, "filter": ["all", ["==", ["get", "structure"], "tunnel"], ["match", ["get", "class"], ["motorway", "trunk"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "tunnel-motorway-trunk", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, 0.75, 18, 32], "line-color": "rgb(235, 235, 235)"}, "source-layer": "road"}, {"minzoom": 13, "layout": {"icon-image": "turning-circle-outline", "icon-size": ["interpolate", ["exponential", 1.5], ["zoom"], 14, 0.122, 18, 0.969, 20, 1], "icon-allow-overlap": true, "icon-ignore-placement": true, "icon-padding": 0, "icon-rotation-alignment": "map", "visibility": "none"}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["==", ["geometry-type"], "Point"], ["match", ["get", "class"], ["turning_circle", "turning_loop"], true, false]], "type": "symbol", "source": "composite", "id": "turning-feature-outline", "paint": {}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["step", ["zoom"], ["==", ["get", "class"], "track"], 1, ["match", ["get", "class"], ["track", "secondary_link", "tertiary_link", "service"], true, false]], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "road-minor-low", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 14, ["match", ["get", "class"], "track", 1, 0.5], 18, 12], "line-color": "rgb(255, 255, 255)", "line-opacity": ["step", ["zoom"], 1, 14, 0]}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["step", ["zoom"], ["==", ["get", "class"], "track"], 1, ["match", ["get", "class"], ["track", "secondary_link", "tertiary_link", "service"], true, false]], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "road-minor-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.75, 20, 2], "line-color": "rgb(242, 242, 242)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 14, ["match", ["get", "class"], "track", 1, 0.5], 18, 12], "line-opacity": ["step", ["zoom"], 0, 14, 1]}, "source-layer": "road"}, {"minzoom": 11, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["match", ["get", "class"], ["street", "street_limited", "primary_link"], true, false], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "road-street-low", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, 2, 18, 18], "line-color": "rgb(255, 255, 255)", "line-opacity": ["step", ["zoom"], 1, 14, 0]}, "source-layer": "road"}, {"minzoom": 11, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["match", ["get", "class"], ["street", "street_limited", "primary_link"], true, false], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "road-street-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.75, 20, 2], "line-color": "rgb(242, 242, 242)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, 2, 18, 18], "line-opacity": ["step", ["zoom"], 0, 14, 1]}, "source-layer": "road"}, {"minzoom": 8, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["match", ["get", "class"], ["secondary", "tertiary"], true, false], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "road-secondary-tertiary-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 10, 0.75, 18, 2], "line-color": "rgb(242, 242, 242)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, 0.1, 18, 26], "line-opacity": ["step", ["zoom"], 0, 10, 1]}, "source-layer": "road"}, {"minzoom": 7, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["==", ["get", "class"], "primary"], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "road-primary-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 10, 1, 18, 2], "line-color": "rgb(242, 242, 242)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, 0.75, 18, 32], "line-opacity": ["step", ["zoom"], 0, 10, 1]}, "source-layer": "road"}, {"minzoom": 10, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["match", ["get", "class"], ["motorway_link", "trunk_link"], true, false], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "road-major-link-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.75, 20, 2], "line-color": "rgb(242, 242, 242)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, 2, 18, 18], "line-opacity": ["step", ["zoom"], 0, 11, 1]}, "source-layer": "road"}, {"minzoom": 5, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["match", ["get", "class"], ["motorway", "trunk"], true, false], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "road-motorway-trunk-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 10, 1, 18, 2], "line-color": "rgb(242, 242, 242)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, 0.75, 18, 32], "line-opacity": ["step", ["zoom"], ["match", ["get", "class"], "motorway", 1, 0], 6, 1]}, "source-layer": "road"}, {"minzoom": 13, "layout": {}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["==", ["get", "class"], "construction"], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "road-construction", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 14, 2, 18, 18], "line-color": "rgb(255, 255, 255)", "line-dasharray": ["step", ["zoom"], ["literal", [0.4, 0.8]], 15, ["literal", [0.3, 0.6]], 16, ["literal", [0.2, 0.3]], 17, ["literal", [0.2, 0.25]], 18, ["literal", [0.15, 0.15]]]}, "source-layer": "road"}, {"minzoom": 10, "layout": {"line-cap": ["step", ["zoom"], "butt", 13, "round"], "line-join": ["step", ["zoom"], "miter", 13, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["match", ["get", "class"], ["motorway_link", "trunk_link"], true, false], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "road-major-link", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, 2, 18, 18], "line-color": "rgb(255, 255, 255)"}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["step", ["zoom"], ["==", ["get", "class"], "track"], 1, ["match", ["get", "class"], ["track", "secondary_link", "tertiary_link", "service"], true, false]], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "road-minor", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 14, ["match", ["get", "class"], "track", 1, 0.5], 18, 12], "line-color": "rgb(255, 255, 255)", "line-opacity": ["step", ["zoom"], 0, 14, 1]}, "source-layer": "road"}, {"minzoom": 11, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["match", ["get", "class"], ["street", "street_limited", "primary_link"], true, false], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "road-street", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, 2, 18, 18], "line-color": "rgb(255, 255, 255)", "line-opacity": ["step", ["zoom"], 0, 14, 1]}, "source-layer": "road"}, {"minzoom": 8, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["match", ["get", "class"], ["secondary", "tertiary"], true, false], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "road-secondary-tertiary", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, 0.1, 18, 26], "line-color": "rgb(255, 255, 255)"}, "source-layer": "road"}, {"minzoom": 6, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}, "filter": ["all", ["==", ["get", "class"], "primary"], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "road-primary", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, 0.75, 18, 32], "line-color": "rgb(255, 255, 255)"}, "source-layer": "road"}, {"id": "road-motorway-trunk", "type": "line", "source": "composite", "source-layer": "road", "filter": ["all", ["match", ["get", "class"], ["motorway", "trunk"], true, false], ["match", ["get", "structure"], ["none", "ford"], true, false], ["==", ["geometry-type"], "LineString"]], "layout": {"line-cap": ["step", ["zoom"], "butt", 13, "round"], "line-join": ["step", ["zoom"], "miter", 13, "round"]}, "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, 0.75, 18, 32], "line-color": "rgb(255, 255, 255)"}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface"}}, {"minzoom": 13, "layout": {"line-join": "round", "visibility": "none"}, "metadata": {"mapbox:featureComponent": "transit", "mapbox:group": "Transit, surface"}, "filter": ["all", ["match", ["get", "class"], ["major_rail", "minor_rail"], true, false], ["match", ["get", "structure"], ["none", "ford"], true, false]], "type": "line", "source": "composite", "id": "road-rail", "paint": {"line-color": ["interpolate", ["linear"], ["zoom"], 13, "rgb(242, 242, 242)", 17, "rgb(227, 227, 227)"], "line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 14, 0.5, 20, 1]}, "source-layer": "road"}, {"minzoom": 13, "layout": {"icon-image": "turning-circle", "icon-size": ["interpolate", ["exponential", 1.5], ["zoom"], 14, 0.095, 18, 1], "icon-allow-overlap": true, "icon-ignore-placement": true, "icon-padding": 0, "icon-rotation-alignment": "map", "visibility": "none"}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, surface-icons"}, "filter": ["all", ["==", ["geometry-type"], "Point"], ["match", ["get", "class"], ["turning_circle", "turning_loop"], true, false]], "type": "symbol", "source": "composite", "id": "turning-feature", "paint": {}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], ["step", ["zoom"], ["match", ["get", "class"], ["street", "street_limited", "primary_link", "track"], true, false], 1, ["match", ["get", "class"], ["street", "street_limited", "track", "primary_link", "secondary_link", "tertiary_link", "service"], true, false]], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "bridge-street-minor-low", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, ["match", ["get", "class"], ["street", "street_limited", "primary_link"], 2, "track", 1, 0.5], 18, ["match", ["get", "class"], ["street", "street_limited", "primary_link"], 18, 12]], "line-color": "rgb(255, 255, 255)", "line-opacity": ["step", ["zoom"], 1, 14, 0]}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-join": "round"}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], ["step", ["zoom"], ["match", ["get", "class"], ["street", "street_limited", "primary_link", "track"], true, false], 1, ["match", ["get", "class"], ["street", "street_limited", "track", "primary_link", "secondary_link", "tertiary_link", "service"], true, false]], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "bridge-street-minor-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.75, 20, 2], "line-color": "rgb(242, 242, 242)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, ["match", ["get", "class"], ["street", "street_limited", "primary_link"], 2, "track", 1, 0.5], 18, ["match", ["get", "class"], ["street", "street_limited", "primary_link"], 18, 12]], "line-opacity": ["step", ["zoom"], 0, 14, 1]}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-join": "round"}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], ["match", ["get", "class"], ["primary", "secondary", "tertiary"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "bridge-primary-secondary-tertiary-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 10, ["match", ["get", "class"], "primary", 1, 0.75], 18, 2], "line-color": "rgb(242, 242, 242)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, ["match", ["get", "class"], "primary", 0.75, 0.1], 18, ["match", ["get", "class"], "primary", 32, 26]], "line-opacity": ["step", ["zoom"], 0, 10, 1]}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-join": "round"}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], ["match", ["get", "class"], ["motorway_link", "trunk_link"], true, false], ["<=", ["get", "layer"], 1], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "bridge-major-link-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.75, 20, 2], "line-color": "rgb(242, 242, 242)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, 2, 18, 18]}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-join": "round"}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], ["match", ["get", "class"], ["motorway", "trunk"], true, false], ["<=", ["get", "layer"], 1], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "bridge-motorway-trunk-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 10, 1, 18, 2], "line-color": "rgb(242, 242, 242)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, 0.75, 18, 32]}, "source-layer": "road"}, {"minzoom": 13, "layout": {}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], ["==", ["get", "class"], "construction"], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "bridge-construction", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 14, 2, 18, 18], "line-color": "rgb(255, 255, 255)", "line-dasharray": ["step", ["zoom"], ["literal", [0.4, 0.8]], 15, ["literal", [0.3, 0.6]], 16, ["literal", [0.2, 0.3]], 17, ["literal", [0.2, 0.25]], 18, ["literal", [0.15, 0.15]]]}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-cap": "round", "line-join": "round"}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], ["match", ["get", "class"], ["motorway_link", "trunk_link"], true, false], ["<=", ["get", "layer"], 1], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "bridge-major-link", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, 2, 18, 18], "line-color": "rgb(255, 255, 255)"}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], ["step", ["zoom"], ["match", ["get", "class"], ["street", "street_limited", "primary_link", "track"], true, false], 1, ["match", ["get", "class"], ["street", "street_limited", "track", "primary_link", "secondary_link", "tertiary_link", "service"], true, false]], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "bridge-street-minor", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, ["match", ["get", "class"], ["street", "street_limited", "primary_link"], 2, "track", 1, 0.5], 18, ["match", ["get", "class"], ["street", "street_limited", "primary_link"], 18, 12]], "line-color": "rgb(255, 255, 255)", "line-opacity": ["step", ["zoom"], 0, 14, 1]}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], ["match", ["get", "class"], ["primary", "secondary", "tertiary"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "bridge-primary-secondary-tertiary", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, ["match", ["get", "class"], "primary", 0.75, 0.1], 18, ["match", ["get", "class"], "primary", 32, 26]], "line-color": "rgb(255, 255, 255)"}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-cap": "round", "line-join": "round"}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], ["match", ["get", "class"], ["motorway", "trunk"], true, false], ["<=", ["get", "layer"], 1], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "bridge-motorway-trunk", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, 0.75, 18, 32], "line-color": "rgb(255, 255, 255)"}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-join": "round"}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], [">=", ["get", "layer"], 2], ["match", ["get", "class"], ["motorway_link", "trunk_link"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "bridge-major-link-2-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.75, 20, 2], "line-color": "rgb(242, 242, 242)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, 2, 18, 18]}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-join": "round"}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], [">=", ["get", "layer"], 2], ["match", ["get", "class"], ["motorway", "trunk"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "bridge-motorway-trunk-2-case", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 10, 1, 18, 2], "line-color": "rgb(242, 242, 242)", "line-gap-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, 0.75, 18, 32]}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-cap": "round", "line-join": "round"}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], [">=", ["get", "layer"], 2], ["match", ["get", "class"], ["motorway_link", "trunk_link"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "bridge-major-link-2", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 0.5, 14, 2, 18, 18], "line-color": "rgb(255, 255, 255)"}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-cap": ["step", ["zoom"], "butt", 14, "round"], "line-join": ["step", ["zoom"], "miter", 14, "round"]}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], [">=", ["get", "layer"], 2], ["match", ["get", "class"], ["motorway", "trunk"], true, false], ["==", ["geometry-type"], "LineString"]], "type": "line", "source": "composite", "id": "bridge-motorway-trunk-2", "paint": {"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 5, 0.75, 18, 32], "line-color": "rgb(255, 255, 255)"}, "source-layer": "road"}, {"minzoom": 13, "layout": {"line-join": "round", "visibility": "none"}, "metadata": {"mapbox:featureComponent": "transit", "mapbox:group": "Transit, bridges"}, "filter": ["all", ["==", ["get", "structure"], "bridge"], ["match", ["get", "class"], ["major_rail", "minor_rail"], true, false]], "type": "line", "source": "composite", "id": "bridge-rail", "paint": {"line-color": "rgb(227, 227, 227)", "line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 14, 0.5, 20, 1]}, "source-layer": "road"}, {"minzoom": 7, "layout": {"line-join": "bevel", "visibility": "none"}, "metadata": {"mapbox:featureComponent": "admin-boundaries", "mapbox:group": "Administrative boundaries, admin"}, "filter": ["all", ["==", ["get", "admin_level"], 1], ["==", ["get", "maritime"], "false"], ["match", ["get", "worldview"], ["all", "US"], true, false]], "type": "line", "source": "composite", "id": "admin-1-boundary-bg", "paint": {"line-color": ["interpolate", ["linear"], ["zoom"], 8, "rgb(227, 227, 227)", 16, "rgb(227, 227, 227)"], "line-width": ["interpolate", ["linear"], ["zoom"], 7, 3.75, 12, 5.5], "line-opacity": ["interpolate", ["linear"], ["zoom"], 7, 0, 8, 0.75], "line-dasharray": [1, 0], "line-blur": ["interpolate", ["linear"], ["zoom"], 3, 0, 8, 3]}, "source-layer": "admin"}, {"minzoom": 1, "layout": {"visibility": "none"}, "metadata": {"mapbox:featureComponent": "admin-boundaries", "mapbox:group": "Administrative boundaries, admin"}, "filter": ["all", ["==", ["get", "admin_level"], 0], ["==", ["get", "maritime"], "false"], ["match", ["get", "worldview"], ["all", "US"], true, false]], "type": "line", "source": "composite", "id": "admin-0-boundary-bg", "paint": {"line-width": ["interpolate", ["linear"], ["zoom"], 3, 3.5, 10, 8], "line-color": "rgb(227, 227, 227)", "line-opacity": ["interpolate", ["linear"], ["zoom"], 3, 0, 4, 0.5], "line-blur": ["interpolate", ["linear"], ["zoom"], 3, 0, 10, 2]}, "source-layer": "admin"}, {"minzoom": 2, "layout": {"line-join": "round", "line-cap": "round", "visibility": "none"}, "metadata": {"mapbox:featureComponent": "admin-boundaries", "mapbox:group": "Administrative boundaries, admin"}, "filter": ["all", ["==", ["get", "admin_level"], 1], ["==", ["get", "maritime"], "false"], ["match", ["get", "worldview"], ["all", "US"], true, false]], "type": "line", "source": "composite", "id": "admin-1-boundary", "paint": {"line-dasharray": ["step", ["zoom"], ["literal", [2, 0]], 7, ["literal", [2, 2, 6, 2]]], "line-width": ["interpolate", ["linear"], ["zoom"], 7, 0.75, 12, 1.5], "line-opacity": ["interpolate", ["linear"], ["zoom"], 2, 0, 3, 1], "line-color": ["interpolate", ["linear"], ["zoom"], 3, "rgb(224, 224, 224)", 7, "rgb(184, 184, 184)"]}, "source-layer": "admin"}, {"minzoom": 1, "layout": {"line-join": "round", "line-cap": "round", "visibility": "none"}, "metadata": {"mapbox:featureComponent": "admin-boundaries", "mapbox:group": "Administrative boundaries, admin"}, "filter": ["all", ["==", ["get", "admin_level"], 0], ["==", ["get", "disputed"], "false"], ["==", ["get", "maritime"], "false"], ["match", ["get", "worldview"], ["all", "US"], true, false]], "type": "line", "source": "composite", "id": "admin-0-boundary", "paint": {"line-color": "rgb(184, 184, 184)", "line-width": ["interpolate", ["linear"], ["zoom"], 3, 0.5, 10, 2], "line-dasharray": [10, 0]}, "source-layer": "admin"}, {"minzoom": 1, "layout": {"line-join": "round", "visibility": "none"}, "metadata": {"mapbox:featureComponent": "admin-boundaries", "mapbox:group": "Administrative boundaries, admin"}, "filter": ["all", ["==", ["get", "disputed"], "true"], ["==", ["get", "admin_level"], 0], ["==", ["get", "maritime"], "false"], ["match", ["get", "worldview"], ["all", "US"], true, false]], "type": "line", "source": "composite", "id": "admin-0-boundary-disputed", "paint": {"line-color": "rgb(184, 184, 184)", "line-width": ["interpolate", ["linear"], ["zoom"], 3, 0.5, 10, 2], "line-dasharray": ["step", ["zoom"], ["literal", [3.25, 3.25]], 6, ["literal", [2.5, 2.5]], 7, ["literal", [2, 2.25]], 8, ["literal", [1.75, 2]]]}, "source-layer": "admin"}, {"minzoom": 10, "layout": {"text-size": ["interpolate", ["linear"], ["zoom"], 10, ["match", ["get", "class"], ["motorway", "trunk", "primary", "secondary", "tertiary"], 10, ["motorway_link", "trunk_link", "primary_link", "secondary_link", "tertiary_link", "street", "street_limited"], 9, 6.5], 18, ["match", ["get", "class"], ["motorway", "trunk", "primary", "secondary", "tertiary"], 16, ["motorway_link", "trunk_link", "primary_link", "secondary_link", "tertiary_link", "street", "street_limited"], 14, 13]], "text-max-angle": 30, "text-font": ["DIN Pro Regular", "Arial Unicode MS Regular"], "symbol-placement": "line", "text-padding": 1, "visibility": "none", "text-rotation-alignment": "map", "text-pitch-alignment": "viewport", "text-field": ["coalesce", ["get", "name_en"], ["get", "name"]], "text-letter-spacing": 0.01}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, road-labels"}, "filter": ["step", ["zoom"], ["match", ["get", "class"], ["motorway", "trunk", "primary", "secondary", "tertiary"], true, false], 1, ["match", ["get", "class"], ["motorway", "trunk", "primary", "secondary", "tertiary", "street", "street_limited"], true, false], 2, ["match", ["get", "class"], ["path", "pedestrian", "golf", "ferry", "aerialway"], false, true]], "type": "symbol", "source": "composite", "id": "road-label", "paint": {"text-color": "rgb(128, 128, 128)", "text-halo-color": "rgb(255, 255, 255)", "text-halo-width": 1, "text-halo-blur": 1}, "source-layer": "road"}, {"minzoom": 13, "layout": {"text-field": ["coalesce", ["get", "name_en"], ["get", "name"]], "icon-image": "intersection", "icon-text-fit": "both", "icon-text-fit-padding": [1, 2, 1, 2], "text-size": ["interpolate", ["exponential", 1.2], ["zoom"], 15, 9, 18, 12], "text-font": ["DIN Pro Bold", "Arial Unicode MS Bold"], "visibility": "none"}, "metadata": {"mapbox:featureComponent": "road-network", "mapbox:group": "Road network, road-labels"}, "filter": ["all", ["==", ["get", "class"], "intersection"], ["has", "name"]], "type": "symbol", "source": "composite", "id": "road-intersection", "paint": {"text-color": "rgb(153, 153, 153)"}, "source-layer": "road"}, {"minzoom": 13, "layout": {"text-font": ["DIN Pro Italic", "Arial Unicode MS Regular"], "text-max-angle": 30, "symbol-spacing": ["interpolate", ["linear", 1], ["zoom"], 15, 250, 17, 400], "text-size": ["interpolate", ["linear"], ["zoom"], 13, 12, 18, 16], "symbol-placement": "line", "text-pitch-alignment": "viewport", "text-field": ["coalesce", ["get", "name_en"], ["get", "name"]], "visibility": "none"}, "metadata": {"mapbox:featureComponent": "natural-features", "mapbox:group": "Natural features, natural-labels"}, "filter": ["all", ["match", ["get", "class"], ["canal", "river", "stream"], ["match", ["get", "worldview"], ["all", "US"], true, false], ["disputed_canal", "disputed_river", "disputed_stream"], ["all", ["==", ["get", "disputed"], "true"], ["match", ["get", "worldview"], ["all", "US"], true, false]], false], ["==", ["geometry-type"], "LineString"]], "type": "symbol", "source": "composite", "id": "waterway-label", "paint": {"text-color": "rgb(150, 150, 150)"}, "source-layer": "natural_label"}, {"minzoom": 4, "layout": {"text-size": ["step", ["zoom"], ["step", ["get", "sizerank"], 18, 5, 12], 17, ["step", ["get", "sizerank"], 18, 13, 12]], "text-max-angle": 30, "text-field": ["coalesce", ["get", "name_en"], ["get", "name"]], "text-font": ["DIN Pro Medium", "Arial Unicode MS Regular"], "symbol-placement": "line-center", "text-pitch-alignment": "viewport", "visibility": "none"}, "metadata": {"mapbox:featureComponent": "natural-features", "mapbox:group": "Natural features, natural-labels"}, "filter": ["all", ["match", ["get", "class"], ["glacier", "landform"], ["match", ["get", "worldview"], ["all", "US"], true, false], ["disputed_glacier", "disputed_landform"], ["all", ["==", ["get", "disputed"], "true"], ["match", ["get", "worldview"], ["all", "US"], true, false]], false], ["==", ["geometry-type"], "LineString"], ["<=", ["get", "filterrank"], 1]], "type": "symbol", "source": "composite", "id": "natural-line-label", "paint": {"text-halo-width": 0.5, "text-halo-color": "rgb(255, 255, 255)", "text-halo-blur": 0.5, "text-color": "rgb(128, 128, 128)"}, "source-layer": "natural_label"}, {"minzoom": 4, "layout": {"text-size": ["step", ["zoom"], ["step", ["get", "sizerank"], 18, 5, 12], 17, ["step", ["get", "sizerank"], 18, 13, 12]], "icon-image": "", "text-font": ["DIN Pro Medium", "Arial Unicode MS Regular"], "text-offset": ["literal", [0, 0]], "text-field": ["coalesce", ["get", "name_en"], ["get", "name"]], "visibility": "none"}, "metadata": {"mapbox:featureComponent": "natural-features", "mapbox:group": "Natural features, natural-labels"}, "filter": ["all", ["match", ["get", "class"], ["dock", "glacier", "landform", "water_feature", "wetland"], ["match", ["get", "worldview"], ["all", "US"], true, false], ["disputed_dock", "disputed_glacier", "disputed_landform", "disputed_water_feature", "disputed_wetland"], ["all", ["==", ["get", "disputed"], "true"], ["match", ["get", "worldview"], ["all", "US"], true, false]], false], ["==", ["geometry-type"], "Point"], ["<=", ["get", "filterrank"], 1]], "type": "symbol", "source": "composite", "id": "natural-point-label", "paint": {"icon-opacity": ["step", ["zoom"], ["step", ["get", "sizerank"], 0, 5, 1], 17, ["step", ["get", "sizerank"], 0, 13, 1]], "text-halo-color": "rgb(255, 255, 255)", "text-halo-width": 0.5, "text-halo-blur": 0.5, "text-color": "rgb(128, 128, 128)"}, "source-layer": "natural_label"}, {"id": "water-line-label", "type": "symbol", "metadata": {"mapbox:featureComponent": "natural-features", "mapbox:group": "Natural features, natural-labels"}, "source": "composite", "source-layer": "natural_label", "filter": ["all", ["match", ["get", "class"], ["bay", "ocean", "reservoir", "sea", "water"], ["match", ["get", "worldview"], ["all", "US"], true, false], ["disputed_bay", "disputed_ocean", "disputed_reservoir", "disputed_sea", "disputed_water"], ["all", ["==", ["get", "disputed"], "true"], ["match", ["get", "worldview"], ["all", "US"], true, false]], false], ["==", ["geometry-type"], "LineString"]], "layout": {"text-size": ["interpolate", ["linear"], ["zoom"], 7, ["step", ["get", "sizerank"], 20, 6, 18, 12, 12], 10, ["step", ["get", "sizerank"], 15, 9, 12], 18, ["step", ["get", "sizerank"], 15, 9, 14]], "text-max-angle": 30, "text-letter-spacing": ["match", ["get", "class"], "ocean", 0.25, ["sea", "bay"], 0.15, 0], "text-font": ["DIN Pro Italic", "Arial Unicode MS Regular"], "symbol-placement": "line-center", "text-pitch-alignment": "viewport", "text-field": ["coalesce", ["get", "name_en"], ["get", "name"]], "visibility": "none"}, "paint": {"text-color": "rgb(150, 150, 150)"}}, {"id": "water-point-label", "type": "symbol", "source": "composite", "source-layer": "natural_label", "filter": ["all", ["match", ["get", "class"], ["bay", "ocean", "reservoir", "sea", "water"], ["match", ["get", "worldview"], ["all", "US"], true, false], ["disputed_bay", "disputed_ocean", "disputed_reservoir", "disputed_sea", "disputed_water"], ["all", ["==", ["get", "disputed"], "true"], ["match", ["get", "worldview"], ["all", "US"], true, false]], false], ["==", ["geometry-type"], "Point"]], "layout": {"text-line-height": 1.3, "text-size": ["interpolate", ["linear"], ["zoom"], 7, ["step", ["get", "sizerank"], 20, 6, 15, 12, 12], 10, ["step", ["get", "sizerank"], 15, 9, 12]], "text-font": ["DIN Pro Italic", "Arial Unicode MS Regular"], "text-field": ["coalesce", ["get", "name_en"], ["get", "name"]], "text-letter-spacing": ["match", ["get", "class"], "ocean", 0.25, ["bay", "sea"], 0.15, 0.01], "text-max-width": ["match", ["get", "class"], "ocean", 4, "sea", 5, ["bay", "water"], 7, 10], "visibility": "none"}, "paint": {"text-color": "rgb(150, 150, 150)"}, "metadata": {"mapbox:featureComponent": "natural-features", "mapbox:group": "Natural features, natural-labels"}}, {"minzoom": 6, "layout": {"text-size": ["step", ["zoom"], ["step", ["get", "sizerank"], 18, 5, 12], 17, ["step", ["get", "sizerank"], 18, 13, 12]], "icon-image": "", "text-font": ["DIN Pro Medium", "Arial Unicode MS Regular"], "text-offset": [0, 0], "text-anchor": ["step", ["zoom"], ["step", ["get", "sizerank"], "center", 5, "top"], 17, ["step", ["get", "sizerank"], "center", 13, "top"]], "text-field": ["coalesce", ["get", "name_en"], ["get", "name"]], "visibility": "none"}, "metadata": {"mapbox:featureComponent": "point-of-interest-labels", "mapbox:group": "Point of interest labels, poi-labels"}, "filter": ["<=", ["get", "filterrank"], ["+", ["step", ["zoom"], 1, 2, 3, 4, 5], 1]], "type": "symbol", "source": "composite", "id": "poi-label", "paint": {"text-halo-color": "rgb(255, 255, 255)", "text-halo-width": 0.5, "text-halo-blur": 0.5, "text-color": ["step", ["zoom"], ["step", ["get", "sizerank"], "rgb(184, 184, 184)", 5, "rgb(161, 161, 161)"], 17, ["step", ["get", "sizerank"], "rgb(184, 184, 184)", 13, "rgb(161, 161, 161)"]]}, "source-layer": "poi_label"}, {"minzoom": 8, "layout": {"text-line-height": 1.1, "text-size": ["step", ["get", "sizerank"], 18, 9, 12], "icon-image": ["get", "maki"], "text-font": ["DIN Pro Medium", "Arial Unicode MS Regular"], "visibility": "none", "text-offset": [0, 0.75], "text-rotation-alignment": "viewport", "text-anchor": "top", "text-field": ["step", ["get", "sizerank"], ["coalesce", ["get", "name_en"], ["get", "name"]], 15, ["get", "ref"]], "text-letter-spacing": 0.01, "text-max-width": 9}, "metadata": {"mapbox:featureComponent": "transit", "mapbox:group": "Transit, transit-labels"}, "filter": ["match", ["get", "class"], ["military", "civil"], ["match", ["get", "worldview"], ["all", "US"], true, false], ["disputed_military", "disputed_civil"], ["all", ["==", ["get", "disputed"], "true"], ["match", ["get", "worldview"], ["all", "US"], true, false]], false], "type": "symbol", "source": "composite", "id": "airport-label", "paint": {"text-color": "rgb(128, 128, 128)", "text-halo-color": "rgb(255, 255, 255)", "text-halo-width": 1}, "source-layer": "airport_label"}, {"minzoom": 10, "layout": {"text-field": ["coalesce", ["get", "name_en"], ["get", "name"]], "text-transform": "uppercase", "text-font": ["DIN Pro Regular", "Arial Unicode MS Regular"], "text-letter-spacing": ["match", ["get", "type"], "suburb", 0.15, 0.1], "text-max-width": 7, "text-padding": 3, "text-size": ["interpolate", ["cubic-bezier", 0.5, 0, 1, 1], ["zoom"], 11, ["match", ["get", "type"], "suburb", 11, 10.5], 15, ["match", ["get", "type"], "suburb", 15, 14]], "visibility": "none"}, "metadata": {"mapbox:featureComponent": "place-labels", "mapbox:group": "Place labels, place-labels"}, "maxzoom": 15, "filter": ["all", ["match", ["get", "class"], "settlement_subdivision", ["match", ["get", "worldview"], ["all", "US"], true, false], "disputed_settlement_subdivision", ["all", ["==", ["get", "disputed"], "true"], ["match", ["get", "worldview"], ["all", "US"], true, false]], false], ["<=", ["get", "filterrank"], 4]], "type": "symbol", "source": "composite", "id": "settlement-subdivision-label", "paint": {"text-halo-color": "rgb(255, 255, 255)", "text-halo-width": 1, "text-color": "rgb(179, 179, 179)", "text-halo-blur": 0.5}, "source-layer": "place_label"}, {"minzoom": 3, "layout": {"text-line-height": 1.1, "text-size": ["interpolate", ["cubic-bezier", 0.2, 0, 0.9, 1], ["zoom"], 3, ["step", ["get", "symbolrank"], 12, 9, 11, 10, 10.5, 12, 9.5, 14, 8.5, 16, 6.5, 17, 4], 13, ["step", ["get", "symbolrank"], 23, 9, 21, 10, 19, 11, 17, 12, 16, 13, 15, 15, 13]], "text-radial-offset": ["step", ["zoom"], ["match", ["get", "capital"], 2, 0.6, 0.55], 8, 0], "icon-image": ["step", ["zoom"], ["case", ["==", ["get", "capital"], 2], "border-dot-13", ["step", ["get", "symbolrank"], "dot-11", 9, "dot-10", 11, "dot-9"]], 8, ""], "text-font": ["DIN Pro Regular", "Arial Unicode MS Regular"], "text-justify": "auto", "visibility": "none", "text-anchor": ["step", ["zoom"], ["get", "text_anchor"], 8, "center"], "text-field": ["coalesce", ["get", "name_en"], ["get", "name"]], "text-max-width": 7}, "metadata": {"mapbox:featureComponent": "place-labels", "mapbox:group": "Place labels, place-labels"}, "maxzoom": 13, "filter": ["all", ["<=", ["get", "filterrank"], 3], ["match", ["get", "class"], "settlement", ["match", ["get", "worldview"], ["all", "US"], true, false], "disputed_settlement", ["all", ["==", ["get", "disputed"], "true"], ["match", ["get", "worldview"], ["all", "US"], true, false]], false], ["step", ["zoom"], [">", ["get", "symbolrank"], 6], 1, [">=", ["get", "symbolrank"], 7], 2, [">=", ["get", "symbolrank"], 8], 3, [">=", ["get", "symbolrank"], 10], 4, [">=", ["get", "symbolrank"], 11], 5, [">=", ["get", "symbolrank"], 13], 6, [">=", ["get", "symbolrank"], 15]]], "type": "symbol", "source": "composite", "id": "settlement-minor-label", "paint": {"text-color": ["step", ["get", "symbolrank"], "rgb(128, 128, 128)", 11, "rgb(161, 161, 161)", 16, "rgb(184, 184, 184)"], "text-halo-color": "rgb(255, 255, 255)", "text-halo-width": 1, "text-halo-blur": 1}, "source-layer": "place_label"}, {"minzoom": 3, "layout": {"text-line-height": 1.1, "text-size": ["interpolate", ["cubic-bezier", 0.2, 0, 0.9, 1], ["zoom"], 3, ["step", ["get", "symbolrank"], 13, 6, 12], 6, ["step", ["get", "symbolrank"], 16, 6, 15, 7, 14], 8, ["step", ["get", "symbolrank"], 18, 9, 17, 10, 15], 15, ["step", ["get", "symbolrank"], 23, 9, 22, 10, 20, 11, 18, 12, 16, 13, 15, 15, 13]], "text-radial-offset": ["step", ["zoom"], ["match", ["get", "capital"], 2, 0.6, 0.55], 8, 0], "icon-image": ["step", ["zoom"], ["case", ["==", ["get", "capital"], 2], "border-dot-13", ["step", ["get", "symbolrank"], "dot-11", 9, "dot-10", 11, "dot-9"]], 8, ""], "text-font": ["DIN Pro Medium", "Arial Unicode MS Regular"], "text-justify": ["step", ["zoom"], ["match", ["get", "text_anchor"], ["left", "bottom-left", "top-left"], "left", ["right", "bottom-right", "top-right"], "right", "center"], 8, "center"], "visibility": "none", "text-anchor": ["step", ["zoom"], ["get", "text_anchor"], 8, "center"], "text-field": ["coalesce", ["get", "name_en"], ["get", "name"]], "text-max-width": 7}, "metadata": {"mapbox:featureComponent": "place-labels", "mapbox:group": "Place labels, place-labels"}, "maxzoom": 15, "filter": ["all", ["<=", ["get", "filterrank"], 3], ["match", ["get", "class"], "settlement", ["match", ["get", "worldview"], ["all", "US"], true, false], "disputed_settlement", ["all", ["==", ["get", "disputed"], "true"], ["match", ["get", "worldview"], ["all", "US"], true, false]], false], ["step", ["zoom"], false, 1, ["<=", ["get", "symbolrank"], 6], 2, ["<", ["get", "symbolrank"], 7], 3, ["<", ["get", "symbolrank"], 8], 4, ["<", ["get", "symbolrank"], 10], 5, ["<", ["get", "symbolrank"], 11], 6, ["<", ["get", "symbolrank"], 13], 7, ["<", ["get", "symbolrank"], 15], 8, [">=", ["get", "symbolrank"], 11], 9, [">=", ["get", "symbolrank"], 15]]], "type": "symbol", "source": "composite", "id": "settlement-major-label", "paint": {"text-color": ["step", ["get", "symbolrank"], "rgb(128, 128, 128)", 11, "rgb(161, 161, 161)", 16, "rgb(184, 184, 184)"], "text-halo-color": "rgb(255, 255, 255)", "text-halo-width": 1, "text-halo-blur": 1}, "source-layer": "place_label"}, {"minzoom": 3, "layout": {"text-size": ["interpolate", ["cubic-bezier", 0.85, 0.7, 0.65, 1], ["zoom"], 4, ["step", ["get", "symbolrank"], 10, 6, 9.5, 7, 9], 9, ["step", ["get", "symbolrank"], 21, 6, 16, 7, 13]], "text-transform": "uppercase", "text-font": ["DIN Pro Bold", "Arial Unicode MS Bold"], "text-field": ["step", ["zoom"], ["step", ["get", "symbolrank"], ["coalesce", ["get", "name_en"], ["get", "name"]], 5, ["coalesce", ["get", "abbr"], ["get", "name_en"], ["get", "name"]]], 5, ["coalesce", ["get", "name_en"], ["get", "name"]]], "text-letter-spacing": 0.15, "text-max-width": 6, "visibility": "none"}, "metadata": {"mapbox:featureComponent": "place-labels", "mapbox:group": "Place labels, place-labels"}, "maxzoom": 9, "filter": ["match", ["get", "class"], "state", ["match", ["get", "worldview"], ["all", "US"], true, false], "disputed_state", ["all", ["==", ["get", "disputed"], "true"], ["match", ["get", "worldview"], ["all", "US"], true, false]], false], "type": "symbol", "source": "composite", "id": "state-label", "paint": {"text-color": "rgb(184, 184, 184)", "text-halo-color": "rgb(255, 255, 255)", "text-halo-width": 1}, "source-layer": "place_label"}, {"minzoom": 1, "layout": {"text-line-height": 1.1, "text-size": ["interpolate", ["cubic-bezier", 0.2, 0, 0.7, 1], ["zoom"], 1, ["step", ["get", "symbolrank"], 11, 4, 9, 5, 8], 9, ["step", ["get", "symbolrank"], 22, 4, 19, 5, 17]], "text-radial-offset": ["step", ["zoom"], 0.6, 8, 0], "icon-image": "", "text-font": ["DIN Pro Medium", "Arial Unicode MS Regular"], "text-justify": ["step", ["zoom"], ["match", ["get", "text_anchor"], ["left", "bottom-left", "top-left"], "left", ["right", "bottom-right", "top-right"], "right", "center"], 7, "auto"], "visibility": "none", "text-field": ["coalesce", ["get", "name_en"], ["get", "name"]], "text-max-width": 6}, "metadata": {"mapbox:featureComponent": "place-labels", "mapbox:group": "Place labels, place-labels"}, "maxzoom": 10, "filter": ["match", ["get", "class"], "country", ["match", ["get", "worldview"], ["all", "US"], true, false], "disputed_country", ["all", ["==", ["get", "disputed"], "true"], ["match", ["get", "worldview"], ["all", "US"], true, false]], false], "type": "symbol", "source": "composite", "id": "country-label", "paint": {"icon-opacity": ["step", ["zoom"], ["case", ["has", "text_anchor"], 1, 0], 7, 0], "text-color": "rgb(128, 128, 128)", "text-halo-color": "rgb(255, 255, 255)", "text-halo-width": 1.25}, "source-layer": "place_label"}], "created": "2021-11-05T16:12:04.822Z", "modified": "2021-11-25T13:58:04.167Z", "id": "ckvmksrpd4n0a14pfdo5heqzr", "owner": "commaai", "visibility": "private", "protected": false, "draft": false} \ No newline at end of file diff --git a/selfdrive/sensord/pigeond.py b/selfdrive/sensord/pigeond.py index e38e2d4c33..f56af1c705 100755 --- a/selfdrive/sensord/pigeond.py +++ b/selfdrive/sensord/pigeond.py @@ -7,14 +7,14 @@ import struct import requests import urllib.parse from datetime import datetime -from typing import List, Optional +from typing import List, Optional, Tuple from cereal import messaging from common.params import Params from system.swaglog import cloudlog -from selfdrive.hardware import TICI +from system.hardware import TICI from common.gpio import gpio_init, gpio_set -from selfdrive.hardware.tici.pins import GPIO +from system.hardware.tici.pins import GPIO UBLOX_TTY = "/dev/ttyHS0" @@ -34,7 +34,6 @@ def set_power(enabled: bool) -> None: gpio_set(GPIO.UBLOX_PWR_EN, enabled) gpio_set(GPIO.UBLOX_RST_N, enabled) - def add_ubx_checksum(msg: bytes) -> bytes: A = B = 0 for b in msg[2:]: @@ -115,35 +114,70 @@ class TTYPigeon(): raise TimeoutError('No response from ublox') time.sleep(0.001) + def reset_device(self) -> bool: + # deleting the backup does not always work on first try (mostly on second try) + for _ in range(5): + # device cold start + self.send(b"\xb5\x62\x06\x04\x04\x00\xff\xff\x00\x00\x0c\x5d") + time.sleep(1) # wait for cold start + init_baudrate(self) + + # clear configuration + self.send_with_ack(b"\xb5\x62\x06\x09\x0d\x00\x00\x00\x1f\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x17\x71\x5b") + + # clear flash memory (almanac backup) + self.send_with_ack(b"\xB5\x62\x09\x14\x04\x00\x01\x00\x00\x00\x22\xf0") + + # try restoring backup to verify it got deleted + self.send(b"\xB5\x62\x09\x14\x00\x00\x1D\x60") + # 1: failed to restore, 2: could restore, 3: no backup + status = self.wait_for_backup_restore_status() + if status == 1 or status == 3: + return True + return False + +def init_baudrate(pigeon: TTYPigeon): + # ublox default setting on startup is 9600 baudrate + pigeon.set_baud(9600) -def initialize_pigeon(pigeon: TTYPigeon) -> None: + # $PUBX,41,1,0007,0003,460800,0*15\r\n + pigeon.send(b"\x24\x50\x55\x42\x58\x2C\x34\x31\x2C\x31\x2C\x30\x30\x30\x37\x2C\x30\x30\x30\x33\x2C\x34\x36\x30\x38\x30\x30\x2C\x30\x2A\x31\x35\x0D\x0A") + time.sleep(0.1) + pigeon.set_baud(460800) + + +def initialize_pigeon(pigeon: TTYPigeon) -> bool: # try initializing a few times for _ in range(10): try: - pigeon.set_baud(9600) - - # up baud rate - pigeon.send(b"\x24\x50\x55\x42\x58\x2C\x34\x31\x2C\x31\x2C\x30\x30\x30\x37\x2C\x30\x30\x30\x33\x2C\x34\x36\x30\x38\x30\x30\x2C\x30\x2A\x31\x35\x0D\x0A") - time.sleep(0.1) - pigeon.set_baud(460800) - - # other configuration messages - pigeon.send_with_ack(b"\xB5\x62\x06\x00\x14\x00\x03\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x1E\x7F") - pigeon.send_with_ack(b"\xB5\x62\x06\x00\x14\x00\x00\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x19\x35") - pigeon.send_with_ack(b"\xB5\x62\x06\x00\x14\x00\x01\x00\x00\x00\xC0\x08\x00\x00\x00\x08\x07\x00\x01\x00\x01\x00\x00\x00\x00\x00\xF4\x80") - pigeon.send_with_ack(b"\xB5\x62\x06\x00\x14\x00\x04\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1D\x85") - pigeon.send_with_ack(b"\xB5\x62\x06\x00\x00\x00\x06\x18") - pigeon.send_with_ack(b"\xB5\x62\x06\x00\x01\x00\x01\x08\x22") - pigeon.send_with_ack(b"\xB5\x62\x06\x00\x01\x00\x03\x0A\x24") + + # setup port config + pigeon.send_with_ack(b"\xb5\x62\x06\x00\x14\x00\x03\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x1E\x7F") + pigeon.send_with_ack(b"\xb5\x62\x06\x00\x14\x00\x00\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x19\x35") + pigeon.send_with_ack(b"\xb5\x62\x06\x00\x14\x00\x01\x00\x00\x00\xC0\x08\x00\x00\x00\x08\x07\x00\x01\x00\x01\x00\x00\x00\x00\x00\xF4\x80") + pigeon.send_with_ack(b"\xb5\x62\x06\x00\x14\x00\x04\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1D\x85") + pigeon.send_with_ack(b"\xb5\x62\x06\x00\x00\x00\x06\x18") + pigeon.send_with_ack(b"\xb5\x62\x06\x00\x01\x00\x01\x08\x22") + pigeon.send_with_ack(b"\xb5\x62\x06\x00\x01\x00\x03\x0A\x24") + + # UBX-CFG-RATE (0x06 0x08) pigeon.send_with_ack(b"\xB5\x62\x06\x08\x06\x00\x64\x00\x01\x00\x00\x00\x79\x10") + + # UBX-CFG-NAV5 (0x06 0x24) pigeon.send_with_ack(b"\xB5\x62\x06\x24\x24\x00\x05\x00\x04\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5A\x63") + + # UBX-CFG-ODO (0x06 0x1E) pigeon.send_with_ack(b"\xB5\x62\x06\x1E\x14\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3C\x37") pigeon.send_with_ack(b"\xB5\x62\x06\x39\x08\x00\xFF\xAD\x62\xAD\x1E\x63\x00\x00\x83\x0C") pigeon.send_with_ack(b"\xB5\x62\x06\x23\x28\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x24") + + # UBX-CFG-NAV5 (0x06 0x24) pigeon.send_with_ack(b"\xB5\x62\x06\x24\x00\x00\x2A\x84") pigeon.send_with_ack(b"\xB5\x62\x06\x23\x00\x00\x29\x81") pigeon.send_with_ack(b"\xB5\x62\x06\x1E\x00\x00\x24\x72") pigeon.send_with_ack(b"\xB5\x62\x06\x39\x00\x00\x3F\xC3") + + # UBX-CFG-MSG (set message rate) pigeon.send_with_ack(b"\xB5\x62\x06\x01\x03\x00\x01\x07\x01\x13\x51") pigeon.send_with_ack(b"\xB5\x62\x06\x01\x03\x00\x02\x15\x01\x22\x70") pigeon.send_with_ack(b"\xB5\x62\x06\x01\x03\x00\x02\x13\x01\x20\x6C") @@ -198,6 +232,10 @@ def initialize_pigeon(pigeon: TTYPigeon) -> None: break except TimeoutError: cloudlog.warning("Initialization failed, trying again!") + else: + cloudlog.warning("Failed to initialize pigeon") + return False + return True def deinitialize_and_exit(pigeon: Optional[TTYPigeon]): cloudlog.warning("Storing almanac in ublox flash") @@ -220,13 +258,11 @@ def deinitialize_and_exit(pigeon: Optional[TTYPigeon]): set_power(False) sys.exit(0) -def main(): - assert TICI, "unsupported hardware for pigeond" +def create_pigeon() -> Tuple[TTYPigeon, messaging.PubMaster]: + pigeon = None # register exit handler - pigeon = None signal.signal(signal.SIGINT, lambda sig, frame: deinitialize_and_exit(pigeon)) - pm = messaging.PubMaster(['ubloxRaw']) # power cycle ublox @@ -236,14 +272,20 @@ def main(): time.sleep(0.5) pigeon = TTYPigeon() - initialize_pigeon(pigeon) + return pigeon, pm - # start receiving data - while True: +def run_receiving(pigeon: TTYPigeon, pm: messaging.PubMaster, duration: int = 0): + + start_time = time.monotonic() + def end_condition(): + return True if duration == 0 else time.monotonic() - start_time < duration + + while end_condition(): dat = pigeon.receive() if len(dat) > 0: if dat[0] == 0x00: cloudlog.warning("received invalid data from ublox, re-initing!") + init_baudrate(pigeon) initialize_pigeon(pigeon) continue @@ -251,6 +293,21 @@ def main(): msg = messaging.new_message('ubloxRaw', len(dat)) msg.ubloxRaw = dat[:] pm.send('ubloxRaw', msg) + else: + # prevent locking up a CPU core if ublox disconnects + time.sleep(0.001) + + +def main(): + assert TICI, "unsupported hardware for pigeond" + + pigeon, pm = create_pigeon() + init_baudrate(pigeon) + r = initialize_pigeon(pigeon) + Params().put_bool("UbloxAvailable", r) + + # start receiving data + run_receiving(pigeon, pm) if __name__ == "__main__": main() diff --git a/selfdrive/sensord/rawgps/modemdiag.py b/selfdrive/sensord/rawgps/modemdiag.py index cc2bc5b261..5d72aeba9e 100644 --- a/selfdrive/sensord/rawgps/modemdiag.py +++ b/selfdrive/sensord/rawgps/modemdiag.py @@ -1,5 +1,3 @@ -import os -import time import select from serial import Serial from crcmod import mkCrcFun @@ -11,18 +9,7 @@ class ModemDiag: self.pend = b'' def open_serial(self): - def op(): - return Serial("/dev/ttyUSB0", baudrate=115200, rtscts=True, dsrdtr=True, timeout=0) - try: - serial = op() - except Exception: - # TODO: this is a hack to get around modemmanager's exclusive open - print("unlocking serial...") - os.system('sudo su -c \'echo "1-1.1:1.0" > /sys/bus/usb/drivers/option/unbind\'') - os.system('sudo su -c \'echo "1-1.1:1.0" > /sys/bus/usb/drivers/option/bind\'') - time.sleep(0.5) - os.system("sudo chmod 666 /dev/ttyUSB0") - serial = op() + serial = Serial("/dev/ttyUSB0", baudrate=115200, rtscts=True, dsrdtr=True, timeout=0, exclusive=True) serial.flush() serial.reset_input_buffer() serial.reset_output_buffer() diff --git a/selfdrive/sensord/rawgps/rawgpsd.py b/selfdrive/sensord/rawgps/rawgpsd.py index 7c4582902b..5a6827a759 100755 --- a/selfdrive/sensord/rawgps/rawgpsd.py +++ b/selfdrive/sensord/rawgps/rawgpsd.py @@ -5,23 +5,34 @@ import signal import itertools import math import time +import subprocess from typing import NoReturn from struct import unpack_from, calcsize, pack -import cereal.messaging as messaging + from cereal import log -from system.swaglog import cloudlog +import cereal.messaging as messaging from laika.gps_time import GPSTime - +from system.swaglog import cloudlog from selfdrive.sensord.rawgps.modemdiag import ModemDiag, DIAG_LOG_F, setup_logs, send_recv -from selfdrive.sensord.rawgps.structs import dict_unpacker -from selfdrive.sensord.rawgps.structs import gps_measurement_report, gps_measurement_report_sv -from selfdrive.sensord.rawgps.structs import glonass_measurement_report, glonass_measurement_report_sv -from selfdrive.sensord.rawgps.structs import oemdre_measurement_report, oemdre_measurement_report_sv -from selfdrive.sensord.rawgps.structs import LOG_GNSS_GPS_MEASUREMENT_REPORT, LOG_GNSS_GLONASS_MEASUREMENT_REPORT -from selfdrive.sensord.rawgps.structs import position_report, LOG_GNSS_POSITION_REPORT, LOG_GNSS_OEMDRE_MEASUREMENT_REPORT +from selfdrive.sensord.rawgps.structs import (dict_unpacker, position_report, relist, + gps_measurement_report, gps_measurement_report_sv, + glonass_measurement_report, glonass_measurement_report_sv, + oemdre_measurement_report, oemdre_measurement_report_sv, oemdre_svpoly_report, + LOG_GNSS_GPS_MEASUREMENT_REPORT, LOG_GNSS_GLONASS_MEASUREMENT_REPORT, + LOG_GNSS_POSITION_REPORT, LOG_GNSS_OEMDRE_MEASUREMENT_REPORT, + LOG_GNSS_OEMDRE_SVPOLY_REPORT) DEBUG = int(os.getenv("DEBUG", "0"))==1 +LOG_TYPES = [ + LOG_GNSS_GPS_MEASUREMENT_REPORT, + LOG_GNSS_GLONASS_MEASUREMENT_REPORT, + LOG_GNSS_OEMDRE_MEASUREMENT_REPORT, + LOG_GNSS_POSITION_REPORT, + LOG_GNSS_OEMDRE_SVPOLY_REPORT, +] + + miscStatusFields = { "multipathEstimateIsValid": 0, "directionIsValid": 1, @@ -65,59 +76,43 @@ measurementStatusGlonassFields = { "glonassTimeMarkValid": 17 } -def main() -> NoReturn: - unpack_gps_meas, size_gps_meas = dict_unpacker(gps_measurement_report, True) - unpack_gps_meas_sv, size_gps_meas_sv = dict_unpacker(gps_measurement_report_sv, True) - - unpack_glonass_meas, size_glonass_meas = dict_unpacker(glonass_measurement_report, True) - unpack_glonass_meas_sv, size_glonass_meas_sv = dict_unpacker(glonass_measurement_report_sv, True) - unpack_oemdre_meas, size_oemdre_meas = dict_unpacker(oemdre_measurement_report, True) - unpack_oemdre_meas_sv, size_oemdre_meas_sv = dict_unpacker(oemdre_measurement_report_sv, True) - - log_types = [ - LOG_GNSS_GPS_MEASUREMENT_REPORT, - LOG_GNSS_GLONASS_MEASUREMENT_REPORT, - LOG_GNSS_OEMDRE_MEASUREMENT_REPORT, - ] - pub_types = ['qcomGnss'] - unpack_position, _ = dict_unpacker(position_report) - log_types.append(LOG_GNSS_POSITION_REPORT) - pub_types.append("gpsLocation") - - # connect to modem - diag = ModemDiag() - - # NV enable OEMDRE +def try_setup_logs(diag, log_types): + for _ in range(5): + try: + setup_logs(diag, log_types) + break + except Exception: + cloudlog.exception("setup logs failed, trying again") + else: + raise Exception(f"setup logs failed, {log_types=}") + +def mmcli(cmd: str) -> None: + for _ in range(5): + try: + subprocess.check_call(f"mmcli -m any --timeout 30 {cmd}", shell=True) + break + except subprocess.CalledProcessError: + cloudlog.exception("rawgps.mmcli_command_failed") + else: + raise Exception(f"failed to execute mmcli command {cmd=}") + +def setup_quectel(diag: ModemDiag): + # enable OEMDRE in the NV # TODO: it has to reboot for this to take effect DIAG_NV_READ_F = 38 DIAG_NV_WRITE_F = 39 NV_GNSS_OEM_FEATURE_MASK = 7165 + send_recv(diag, DIAG_NV_WRITE_F, pack(' NoReturn: GPSDIAG_OEM_DRE_ON = 1 # gpsdiag_OemControlReqType - opcode, payload = send_recv(diag, DIAG_SUBSYS_CMD_F, pack(' NoReturn: + unpack_gps_meas, size_gps_meas = dict_unpacker(gps_measurement_report, True) + unpack_gps_meas_sv, size_gps_meas_sv = dict_unpacker(gps_measurement_report_sv, True) + + unpack_glonass_meas, size_glonass_meas = dict_unpacker(glonass_measurement_report, True) + unpack_glonass_meas_sv, size_glonass_meas_sv = dict_unpacker(glonass_measurement_report_sv, True) + + unpack_oemdre_meas, size_oemdre_meas = dict_unpacker(oemdre_measurement_report, True) + unpack_oemdre_meas_sv, size_oemdre_meas_sv = dict_unpacker(oemdre_measurement_report_sv, True) + + unpack_svpoly, _ = dict_unpacker(oemdre_svpoly_report, True) + unpack_position, _ = dict_unpacker(position_report) + + unpack_position, _ = dict_unpacker(position_report) + + # wait for ModemManager to come up + cloudlog.warning("waiting for modem to come up") + while True: + ret = subprocess.call("mmcli -m any --timeout 10 --location-status", stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True) + if ret == 0: + break + time.sleep(0.1) + + # connect to modem + diag = ModemDiag() + + def cleanup(sig, frame): + cloudlog.warning(f"caught sig {sig}, disabling quectel gps") + teardown_quectel(diag) + cloudlog.warning("quectel cleanup done") + sys.exit(0) + signal.signal(signal.SIGINT, cleanup) + signal.signal(signal.SIGTERM, cleanup) + + setup_quectel(diag) + cloudlog.warning("quectel setup done") + + pm = messaging.PubMaster(['qcomGnss', 'gpsLocation']) while 1: opcode, payload = diag.recv() - assert opcode == DIAG_LOG_F + if opcode != DIAG_LOG_F: + cloudlog.error(f"Unhandled opcode: {opcode}") + continue + (pending_msgs, log_outer_length), inner_log_packet = unpack_from(' 0: cloudlog.debug("have %d pending messages" % pending_msgs) assert log_outer_length == len(inner_log_packet) + (log_inner_length, log_type, log_time), log_payload = unpack_from(' NoReturn: pm.send('gpsLocation', msg) - if log_type in [LOG_GNSS_GPS_MEASUREMENT_REPORT, LOG_GNSS_GLONASS_MEASUREMENT_REPORT]: + elif log_type == LOG_GNSS_OEMDRE_SVPOLY_REPORT: + msg = messaging.new_message('qcomGnss') + dat = unpack_svpoly(log_payload) + dat = relist(dat) + gnss = msg.qcomGnss + gnss.logTs = log_time + gnss.init('drSvPoly') + poly = gnss.drSvPoly + for k,v in dat.items(): + if k == "version": + assert v == 2 + elif k == "flags": + pass + else: + setattr(poly, k, v) + pm.send('qcomGnss', msg) + + elif log_type in [LOG_GNSS_GPS_MEASUREMENT_REPORT, LOG_GNSS_GLONASS_MEASUREMENT_REPORT]: msg = messaging.new_message('qcomGnss') gnss = msg.qcomGnss diff --git a/selfdrive/sensord/rawgps/structs.py b/selfdrive/sensord/rawgps/structs.py index 4bc9eca875..97e3d3d605 100644 --- a/selfdrive/sensord/rawgps/structs.py +++ b/selfdrive/sensord/rawgps/structs.py @@ -56,6 +56,29 @@ oemdre_measurement_report = """ uint8_t source; """ +oemdre_svpoly_report = """ + uint8_t version; + uint16_t sv_id; + int8_t frequency_index; + uint8_t flags; + uint16_t iode; + double t0; + double xyz0[3]; + double xyzN[9]; + float other[4]; + float position_uncertainty; + float iono_delay; + float iono_dot; + float sbas_iono_delay; + float sbas_iono_dot; + float tropo_delay; + float elevation; + float elevation_dot; + float elevation_uncertainty; + double velocity_coeff[12]; +""" + + oemdre_measurement_report_sv = """ uint8_t sv_id; uint8_t unkn; @@ -311,3 +334,21 @@ def dict_unpacker(ss, camelcase = False): nams = [name_to_camelcase(x) for x in nams] sz = calcsize(st) return lambda x: dict(zip(nams, unpack_from(st, x))), sz + +def relist(dat): + list_keys = set() + for key in dat.keys(): + if '[' in key: + list_keys.add(key.split('[')[0]) + list_dict = {} + for list_key in list_keys: + list_dict[list_key] = [] + i = 0 + while True: + key = list_key + f'[{i}]' + if key not in dat: + break + list_dict[list_key].append(dat[key]) + del dat[key] + i += 1 + return {**dat, **list_dict} diff --git a/selfdrive/sensord/rawgps/test_rawgps.py b/selfdrive/sensord/rawgps/test_rawgps.py new file mode 100755 index 0000000000..5bd0833955 --- /dev/null +++ b/selfdrive/sensord/rawgps/test_rawgps.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +import json +import time +import unittest +import subprocess + +import cereal.messaging as messaging +from system.hardware import TICI +from selfdrive.manager.process_config import managed_processes + + +class TestRawgpsd(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not TICI: + raise unittest.SkipTest + + def tearDown(self): + managed_processes['rawgpsd'].stop() + + def test_startup_time(self): + for _ in range(5): + sm = messaging.SubMaster(['qcomGnss']) + managed_processes['rawgpsd'].start() + + start_time = time.monotonic() + for __ in range(10): + sm.update(1 * 1000) + if sm.updated['qcomGnss']: + break + assert sm.rcv_frame['qcomGnss'] > 0, "rawgpsd didn't start outputting messages in time" + + et = time.monotonic() - start_time + assert et < 5, f"rawgpsd took {et:.1f}s to start" + managed_processes['rawgpsd'].stop() + + def test_turns_off_gnss(self): + for s in (0.1, 0.5, 1, 5): + managed_processes['rawgpsd'].start() + time.sleep(s) + managed_processes['rawgpsd'].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'} + + +if __name__ == "__main__": + unittest.main() diff --git a/selfdrive/sensord/sensors/bmx055_accel.cc b/selfdrive/sensord/sensors/bmx055_accel.cc index c6dcdbd7aa..78b3ac526d 100644 --- a/selfdrive/sensord/sensors/bmx055_accel.cc +++ b/selfdrive/sensord/sensors/bmx055_accel.cc @@ -42,6 +42,7 @@ int BMX055_Accel::init() { if (ret < 0) { goto fail; } + ret = set_register(BMX055_ACCEL_I2C_REG_BW, BMX055_ACCEL_BW_125HZ); if (ret < 0) { goto fail; @@ -61,7 +62,7 @@ int BMX055_Accel::shutdown() { return ret; } -bool BMX055_Accel::get_event(cereal::SensorEventData::Builder &event) { +bool BMX055_Accel::get_event(MessageBuilder &msg, uint64_t ts) { uint64_t start_time = nanos_since_boot(); uint8_t buffer[6]; int len = read_register(BMX055_ACCEL_I2C_REG_X_LSB, buffer, sizeof(buffer)); @@ -73,6 +74,7 @@ bool BMX055_Accel::get_event(cereal::SensorEventData::Builder &event) { float y = -read_12_bit(buffer[2], buffer[3]) * scale; float z = read_12_bit(buffer[4], buffer[5]) * scale; + auto event = msg.initEvent().initAccelerometer2(); event.setSource(cereal::SensorEventData::SensorSource::BMX055); event.setVersion(1); event.setSensor(SENSOR_ACCELEROMETER); diff --git a/selfdrive/sensord/sensors/bmx055_accel.h b/selfdrive/sensord/sensors/bmx055_accel.h index 6a0f9f1ada..8ef660a99f 100644 --- a/selfdrive/sensord/sensors/bmx055_accel.h +++ b/selfdrive/sensord/sensors/bmx055_accel.h @@ -36,6 +36,6 @@ class BMX055_Accel : public I2CSensor { public: BMX055_Accel(I2CBus *bus); int init(); - bool get_event(cereal::SensorEventData::Builder &event); + bool get_event(MessageBuilder &msg, uint64_t ts = 0); int shutdown(); }; diff --git a/selfdrive/sensord/sensors/bmx055_gyro.cc b/selfdrive/sensord/sensors/bmx055_gyro.cc index 4deb15ec6d..9d70b9e431 100644 --- a/selfdrive/sensord/sensors/bmx055_gyro.cc +++ b/selfdrive/sensord/sensors/bmx055_gyro.cc @@ -72,7 +72,7 @@ int BMX055_Gyro::shutdown() { return ret; } -bool BMX055_Gyro::get_event(cereal::SensorEventData::Builder &event) { +bool BMX055_Gyro::get_event(MessageBuilder &msg, uint64_t ts) { uint64_t start_time = nanos_since_boot(); uint8_t buffer[6]; int len = read_register(BMX055_GYRO_I2C_REG_RATE_X_LSB, buffer, sizeof(buffer)); @@ -84,6 +84,7 @@ bool BMX055_Gyro::get_event(cereal::SensorEventData::Builder &event) { float y = -DEG2RAD(read_16_bit(buffer[2], buffer[3]) * scale); float z = DEG2RAD(read_16_bit(buffer[4], buffer[5]) * scale); + auto event = msg.initEvent().initGyroscope2(); event.setSource(cereal::SensorEventData::SensorSource::BMX055); event.setVersion(1); event.setSensor(SENSOR_GYRO_UNCALIBRATED); @@ -94,5 +95,6 @@ bool BMX055_Gyro::get_event(cereal::SensorEventData::Builder &event) { auto svec = event.initGyroUncalibrated(); svec.setV(xyz); svec.setStatus(true); + return true; } diff --git a/selfdrive/sensord/sensors/bmx055_gyro.h b/selfdrive/sensord/sensors/bmx055_gyro.h index ac5dacc4a6..80b93f128c 100644 --- a/selfdrive/sensord/sensors/bmx055_gyro.h +++ b/selfdrive/sensord/sensors/bmx055_gyro.h @@ -36,6 +36,6 @@ class BMX055_Gyro : public I2CSensor { public: BMX055_Gyro(I2CBus *bus); int init(); - bool get_event(cereal::SensorEventData::Builder &event); + bool get_event(MessageBuilder &msg, uint64_t ts = 0); int shutdown(); }; diff --git a/selfdrive/sensord/sensors/bmx055_magn.cc b/selfdrive/sensord/sensors/bmx055_magn.cc index 74e18b7c82..394b1e83d3 100644 --- a/selfdrive/sensord/sensors/bmx055_magn.cc +++ b/selfdrive/sensord/sensors/bmx055_magn.cc @@ -223,7 +223,7 @@ bool BMX055_Magn::parse_xyz(uint8_t buffer[8], int16_t *x, int16_t *y, int16_t * } -bool BMX055_Magn::get_event(cereal::SensorEventData::Builder &event) { +bool BMX055_Magn::get_event(MessageBuilder &msg, uint64_t ts) { uint64_t start_time = nanos_since_boot(); uint8_t buffer[8]; int16_t _x, _y, x, y, z; @@ -233,6 +233,8 @@ bool BMX055_Magn::get_event(cereal::SensorEventData::Builder &event) { bool parsed = parse_xyz(buffer, &_x, &_y, &z); if (parsed) { + + auto event = msg.initEvent().initMagnetometer(); event.setSource(cereal::SensorEventData::SensorSource::BMX055); event.setVersion(2); event.setSensor(SENSOR_MAGNETOMETER_UNCALIBRATED); diff --git a/selfdrive/sensord/sensors/bmx055_magn.h b/selfdrive/sensord/sensors/bmx055_magn.h index 0549e163f6..e4a79bc7e0 100644 --- a/selfdrive/sensord/sensors/bmx055_magn.h +++ b/selfdrive/sensord/sensors/bmx055_magn.h @@ -59,6 +59,6 @@ class BMX055_Magn : public I2CSensor{ public: BMX055_Magn(I2CBus *bus); int init(); - bool get_event(cereal::SensorEventData::Builder &event); + bool get_event(MessageBuilder &msg, uint64_t ts = 0); int shutdown(); }; diff --git a/selfdrive/sensord/sensors/bmx055_temp.cc b/selfdrive/sensord/sensors/bmx055_temp.cc index 3cee34ef19..bdb34f1508 100644 --- a/selfdrive/sensord/sensors/bmx055_temp.cc +++ b/selfdrive/sensord/sensors/bmx055_temp.cc @@ -28,7 +28,7 @@ fail: return ret; } -bool BMX055_Temp::get_event(cereal::SensorEventData::Builder &event) { +bool BMX055_Temp::get_event(MessageBuilder &msg, uint64_t ts) { uint64_t start_time = nanos_since_boot(); uint8_t buffer[1]; int len = read_register(BMX055_ACCEL_I2C_REG_TEMP, buffer, sizeof(buffer)); @@ -36,10 +36,12 @@ bool BMX055_Temp::get_event(cereal::SensorEventData::Builder &event) { float temp = 23.0f + int8_t(buffer[0]) / 2.0f; + auto event = msg.initEvent().initTemperatureSensor(); event.setSource(cereal::SensorEventData::SensorSource::BMX055); event.setVersion(1); event.setType(SENSOR_TYPE_AMBIENT_TEMPERATURE); event.setTimestamp(start_time); event.setTemperature(temp); + return true; } diff --git a/selfdrive/sensord/sensors/bmx055_temp.h b/selfdrive/sensord/sensors/bmx055_temp.h index f5d771a29c..0b6802deaa 100644 --- a/selfdrive/sensord/sensors/bmx055_temp.h +++ b/selfdrive/sensord/sensors/bmx055_temp.h @@ -8,6 +8,6 @@ class BMX055_Temp : public I2CSensor { public: BMX055_Temp(I2CBus *bus); int init(); - bool get_event(cereal::SensorEventData::Builder &event); + bool get_event(MessageBuilder &msg, uint64_t ts = 0); int shutdown() { return 0; } }; diff --git a/selfdrive/sensord/sensors/file_sensor.cc b/selfdrive/sensord/sensors/file_sensor.cc index 80e5121564..a74ae1ae1e 100644 --- a/selfdrive/sensord/sensors/file_sensor.cc +++ b/selfdrive/sensord/sensors/file_sensor.cc @@ -2,8 +2,7 @@ #include -FileSensor::FileSensor(std::string filename) : file(filename) { -} +FileSensor::FileSensor(std::string filename) : file(filename) {} int FileSensor::init() { return file.is_open() ? 0 : 1; diff --git a/selfdrive/sensord/sensors/file_sensor.h b/selfdrive/sensord/sensors/file_sensor.h index 5bcaee66a8..39d695167d 100644 --- a/selfdrive/sensord/sensors/file_sensor.h +++ b/selfdrive/sensord/sensors/file_sensor.h @@ -15,5 +15,5 @@ public: ~FileSensor(); int init(); bool has_interrupt_enabled(); - virtual bool get_event(cereal::SensorEventData::Builder &event) = 0; + virtual bool get_event(MessageBuilder &msg, uint64_t ts = 0) = 0; }; diff --git a/selfdrive/sensord/sensors/i2c_sensor.cc b/selfdrive/sensord/sensors/i2c_sensor.cc index 06a216478b..f563f93d2b 100644 --- a/selfdrive/sensord/sensors/i2c_sensor.cc +++ b/selfdrive/sensord/sensors/i2c_sensor.cc @@ -15,7 +15,8 @@ int32_t read_20_bit(uint8_t b2, uint8_t b1, uint8_t b0) { return int32_t(combined) / (1 << 4); } -I2CSensor::I2CSensor(I2CBus *bus, int gpio_nr, bool shared_gpio) : bus(bus), gpio_nr(gpio_nr), shared_gpio(shared_gpio) {} +I2CSensor::I2CSensor(I2CBus *bus, int gpio_nr, bool shared_gpio) : + bus(bus), gpio_nr(gpio_nr), shared_gpio(shared_gpio) {} I2CSensor::~I2CSensor() { if (gpio_fd != -1) { diff --git a/selfdrive/sensord/sensors/i2c_sensor.h b/selfdrive/sensord/sensors/i2c_sensor.h index f6820a2471..0de2a98738 100644 --- a/selfdrive/sensord/sensors/i2c_sensor.h +++ b/selfdrive/sensord/sensors/i2c_sensor.h @@ -31,6 +31,6 @@ public: int init_gpio(); bool has_interrupt_enabled(); virtual int init() = 0; - virtual bool get_event(cereal::SensorEventData::Builder &event) = 0; + virtual bool get_event(MessageBuilder &msg, uint64_t ts = 0) = 0; virtual int shutdown() = 0; }; diff --git a/selfdrive/sensord/sensors/light_sensor.cc b/selfdrive/sensord/sensors/light_sensor.cc index 321ac75c7e..58c602ea39 100644 --- a/selfdrive/sensord/sensors/light_sensor.cc +++ b/selfdrive/sensord/sensors/light_sensor.cc @@ -5,7 +5,9 @@ #include "common/timing.h" #include "selfdrive/sensord/sensors/constants.h" -bool LightSensor::get_event(cereal::SensorEventData::Builder &event) { +LightSensor::LightSensor(std::string filename) : FileSensor(filename) {} + +bool LightSensor::get_event(MessageBuilder &msg, uint64_t ts) { uint64_t start_time = nanos_since_boot(); file.clear(); file.seekg(0); @@ -13,6 +15,7 @@ bool LightSensor::get_event(cereal::SensorEventData::Builder &event) { int value; file >> value; + auto event = msg.initEvent().initLightSensor(); event.setSource(cereal::SensorEventData::SensorSource::RPR0521); event.setVersion(1); event.setSensor(SENSOR_LIGHT); diff --git a/selfdrive/sensord/sensors/light_sensor.h b/selfdrive/sensord/sensors/light_sensor.h index 63bda74755..7ed1c1f70c 100644 --- a/selfdrive/sensord/sensors/light_sensor.h +++ b/selfdrive/sensord/sensors/light_sensor.h @@ -3,7 +3,7 @@ class LightSensor : public FileSensor { public: - LightSensor(std::string filename) : FileSensor(filename){}; - bool get_event(cereal::SensorEventData::Builder &event); + LightSensor(std::string filename); + bool get_event(MessageBuilder &msg, uint64_t ts = 0); int shutdown() { return 0; } }; diff --git a/selfdrive/sensord/sensors/lsm6ds3_accel.cc b/selfdrive/sensord/sensors/lsm6ds3_accel.cc index 513125fd59..27cd4d0c70 100644 --- a/selfdrive/sensord/sensors/lsm6ds3_accel.cc +++ b/selfdrive/sensord/sensors/lsm6ds3_accel.cc @@ -5,7 +5,8 @@ #include "common/swaglog.h" #include "common/timing.h" -LSM6DS3_Accel::LSM6DS3_Accel(I2CBus *bus, int gpio_nr, bool shared_gpio) : I2CSensor(bus, gpio_nr, shared_gpio) {} +LSM6DS3_Accel::LSM6DS3_Accel(I2CBus *bus, int gpio_nr, bool shared_gpio) : + I2CSensor(bus, gpio_nr, shared_gpio) {} int LSM6DS3_Accel::init() { int ret = 0; @@ -93,15 +94,13 @@ fail: return ret; } -bool LSM6DS3_Accel::get_event(cereal::SensorEventData::Builder &event) { +bool LSM6DS3_Accel::get_event(MessageBuilder &msg, uint64_t ts) { - if (has_interrupt_enabled()) { - // INT1 shared with gyro, check STATUS_REG who triggered - uint8_t status_reg = 0; - read_register(LSM6DS3_ACCEL_I2C_REG_STAT_REG, &status_reg, sizeof(status_reg)); - if ((status_reg & LSM6DS3_ACCEL_DRDY_XLDA) == 0) { - return false; - } + // INT1 shared with gyro, check STATUS_REG who triggered + uint8_t status_reg = 0; + read_register(LSM6DS3_ACCEL_I2C_REG_STAT_REG, &status_reg, sizeof(status_reg)); + if ((status_reg & LSM6DS3_ACCEL_DRDY_XLDA) == 0) { + return false; } uint8_t buffer[6]; @@ -113,10 +112,12 @@ bool LSM6DS3_Accel::get_event(cereal::SensorEventData::Builder &event) { float y = read_16_bit(buffer[2], buffer[3]) * scale; float z = read_16_bit(buffer[4], buffer[5]) * scale; + auto event = msg.initEvent().initAccelerometer(); event.setSource(source); event.setVersion(1); event.setSensor(SENSOR_ACCELEROMETER); event.setType(SENSOR_TYPE_ACCELEROMETER); + event.setTimestamp(ts); float xyz[] = {y, -x, z}; auto svec = event.initAcceleration(); diff --git a/selfdrive/sensord/sensors/lsm6ds3_accel.h b/selfdrive/sensord/sensors/lsm6ds3_accel.h index 6ed94a8f12..84084fc916 100644 --- a/selfdrive/sensord/sensors/lsm6ds3_accel.h +++ b/selfdrive/sensord/sensors/lsm6ds3_accel.h @@ -28,6 +28,6 @@ class LSM6DS3_Accel : public I2CSensor { public: LSM6DS3_Accel(I2CBus *bus, int gpio_nr = 0, bool shared_gpio = false); int init(); - bool get_event(cereal::SensorEventData::Builder &event); + bool get_event(MessageBuilder &msg, uint64_t ts = 0); int shutdown(); }; diff --git a/selfdrive/sensord/sensors/lsm6ds3_gyro.cc b/selfdrive/sensord/sensors/lsm6ds3_gyro.cc index fd0436a5f0..014a72bb73 100644 --- a/selfdrive/sensord/sensors/lsm6ds3_gyro.cc +++ b/selfdrive/sensord/sensors/lsm6ds3_gyro.cc @@ -8,7 +8,8 @@ #define DEG2RAD(x) ((x) * M_PI / 180.0) -LSM6DS3_Gyro::LSM6DS3_Gyro(I2CBus *bus, int gpio_nr, bool shared_gpio) : I2CSensor(bus, gpio_nr, shared_gpio) {} +LSM6DS3_Gyro::LSM6DS3_Gyro(I2CBus *bus, int gpio_nr, bool shared_gpio) : + I2CSensor(bus, gpio_nr, shared_gpio) {} int LSM6DS3_Gyro::init() { int ret = 0; @@ -96,15 +97,13 @@ fail: return ret; } -bool LSM6DS3_Gyro::get_event(cereal::SensorEventData::Builder &event) { +bool LSM6DS3_Gyro::get_event(MessageBuilder &msg, uint64_t ts) { - if (has_interrupt_enabled()) { - // INT1 shared with accel, check STATUS_REG who triggered - uint8_t status_reg = 0; - read_register(LSM6DS3_GYRO_I2C_REG_STAT_REG, &status_reg, sizeof(status_reg)); - if ((status_reg & LSM6DS3_GYRO_DRDY_GDA) == 0) { - return false; - } + // INT1 shared with accel, check STATUS_REG who triggered + uint8_t status_reg = 0; + read_register(LSM6DS3_GYRO_I2C_REG_STAT_REG, &status_reg, sizeof(status_reg)); + if ((status_reg & LSM6DS3_GYRO_DRDY_GDA) == 0) { + return false; } uint8_t buffer[6]; @@ -116,10 +115,12 @@ bool LSM6DS3_Gyro::get_event(cereal::SensorEventData::Builder &event) { float y = DEG2RAD(read_16_bit(buffer[2], buffer[3]) * scale); float z = DEG2RAD(read_16_bit(buffer[4], buffer[5]) * scale); + auto event = msg.initEvent().initGyroscope(); event.setSource(source); event.setVersion(2); event.setSensor(SENSOR_GYRO_UNCALIBRATED); event.setType(SENSOR_TYPE_GYROSCOPE_UNCALIBRATED); + event.setTimestamp(ts); float xyz[] = {y, -x, z}; auto svec = event.initGyroUncalibrated(); diff --git a/selfdrive/sensord/sensors/lsm6ds3_gyro.h b/selfdrive/sensord/sensors/lsm6ds3_gyro.h index c2ed5ab76e..6c61ffcef2 100644 --- a/selfdrive/sensord/sensors/lsm6ds3_gyro.h +++ b/selfdrive/sensord/sensors/lsm6ds3_gyro.h @@ -28,6 +28,6 @@ class LSM6DS3_Gyro : public I2CSensor { public: LSM6DS3_Gyro(I2CBus *bus, int gpio_nr = 0, bool shared_gpio = false); int init(); - bool get_event(cereal::SensorEventData::Builder &event); + bool get_event(MessageBuilder &msg, uint64_t ts = 0); int shutdown(); }; diff --git a/selfdrive/sensord/sensors/lsm6ds3_temp.cc b/selfdrive/sensord/sensors/lsm6ds3_temp.cc index 082ffee08f..7e1c4d4c81 100644 --- a/selfdrive/sensord/sensors/lsm6ds3_temp.cc +++ b/selfdrive/sensord/sensors/lsm6ds3_temp.cc @@ -31,8 +31,7 @@ fail: return ret; } -bool LSM6DS3_Temp::get_event(cereal::SensorEventData::Builder &event) { - +bool LSM6DS3_Temp::get_event(MessageBuilder &msg, uint64_t ts) { uint64_t start_time = nanos_since_boot(); uint8_t buffer[2]; int len = read_register(LSM6DS3_TEMP_I2C_REG_OUT_TEMP_L, buffer, sizeof(buffer)); @@ -41,6 +40,7 @@ bool LSM6DS3_Temp::get_event(cereal::SensorEventData::Builder &event) { float scale = (source == cereal::SensorEventData::SensorSource::LSM6DS3TRC) ? 256.0f : 16.0f; float temp = 25.0f + read_16_bit(buffer[0], buffer[1]) / scale; + auto event = msg.initEvent().initTemperatureSensor(); event.setSource(source); event.setVersion(1); event.setType(SENSOR_TYPE_AMBIENT_TEMPERATURE); diff --git a/selfdrive/sensord/sensors/lsm6ds3_temp.h b/selfdrive/sensord/sensors/lsm6ds3_temp.h index 9d95236901..1d6bcc228a 100644 --- a/selfdrive/sensord/sensors/lsm6ds3_temp.h +++ b/selfdrive/sensord/sensors/lsm6ds3_temp.h @@ -21,6 +21,6 @@ class LSM6DS3_Temp : public I2CSensor { public: LSM6DS3_Temp(I2CBus *bus); int init(); - bool get_event(cereal::SensorEventData::Builder &event); + bool get_event(MessageBuilder &msg, uint64_t ts = 0); int shutdown() { return 0; } }; diff --git a/selfdrive/sensord/sensors/mmc5603nj_magn.cc b/selfdrive/sensord/sensors/mmc5603nj_magn.cc index 8af4956edf..7a9b7a298b 100644 --- a/selfdrive/sensord/sensors/mmc5603nj_magn.cc +++ b/selfdrive/sensord/sensors/mmc5603nj_magn.cc @@ -79,8 +79,7 @@ fail: return ret; } -bool MMC5603NJ_Magn::get_event(cereal::SensorEventData::Builder &event) { - +bool MMC5603NJ_Magn::get_event(MessageBuilder &msg, uint64_t ts) { uint64_t start_time = nanos_since_boot(); uint8_t buffer[9]; int len = read_register(MMC5603NJ_I2C_REG_XOUT0, buffer, sizeof(buffer)); @@ -91,6 +90,7 @@ bool MMC5603NJ_Magn::get_event(cereal::SensorEventData::Builder &event) { float y = read_20_bit(buffer[7], buffer[3], buffer[2]) * scale; float z = read_20_bit(buffer[8], buffer[5], buffer[4]) * scale; + auto event = msg.initEvent().initMagnetometer(); event.setSource(cereal::SensorEventData::SensorSource::MMC5603NJ); event.setVersion(1); event.setSensor(SENSOR_MAGNETOMETER_UNCALIBRATED); diff --git a/selfdrive/sensord/sensors/mmc5603nj_magn.h b/selfdrive/sensord/sensors/mmc5603nj_magn.h index 2c06cab96f..a364c7c37a 100644 --- a/selfdrive/sensord/sensors/mmc5603nj_magn.h +++ b/selfdrive/sensord/sensors/mmc5603nj_magn.h @@ -25,6 +25,6 @@ class MMC5603NJ_Magn : public I2CSensor { public: MMC5603NJ_Magn(I2CBus *bus); int init(); - bool get_event(cereal::SensorEventData::Builder &event); + bool get_event(MessageBuilder &msg, uint64_t ts = 0); int shutdown(); }; diff --git a/selfdrive/sensord/sensors/sensor.h b/selfdrive/sensord/sensors/sensor.h index 0bdc560275..603aa3586e 100644 --- a/selfdrive/sensord/sensors/sensor.h +++ b/selfdrive/sensord/sensors/sensor.h @@ -1,13 +1,18 @@ #pragma once -#include "cereal/gen/cpp/log.capnp.h" +#include "cereal/messaging/messaging.h" class Sensor { public: int gpio_fd = -1; + uint64_t init_delay = 500e6; // default dealy 500ms virtual ~Sensor() {}; virtual int init() = 0; - virtual bool get_event(cereal::SensorEventData::Builder &event) = 0; + virtual bool get_event(MessageBuilder &msg, uint64_t ts = 0) = 0; virtual bool has_interrupt_enabled() = 0; virtual int shutdown() = 0; + + virtual bool is_data_valid(uint64_t st, uint64_t ct) { + return (ct - st) > init_delay; + } }; diff --git a/selfdrive/sensord/sensors_qcom2.cc b/selfdrive/sensord/sensors_qcom2.cc index ded4b5c0b1..2279cf2532 100644 --- a/selfdrive/sensord/sensors_qcom2.cc +++ b/selfdrive/sensord/sensors_qcom2.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -27,18 +28,18 @@ ExitHandler do_exit; std::mutex pm_mutex; - -// filter first values (0.5sec) as those may contain inaccuracies uint64_t init_ts = 0; -constexpr uint64_t init_delay = 500*1e6; -void interrupt_loop(int fd, std::vector& sensors, PubMaster& pm) { +void interrupt_loop(std::vector& sensors, + std::map& sensor_service) +{ + PubMaster pm_int({"gyroscope", "accelerometer"}); + + int fd = sensors[0]->gpio_fd; struct pollfd fd_list[1] = {0}; fd_list[0].fd = fd; fd_list[0].events = POLLIN | POLLPRI; - uint64_t offset = nanos_since_epoch() - nanos_since_boot(); - while (!do_exit) { int err = poll(fd_list, 1, 100); if (err == -1) { @@ -65,39 +66,21 @@ void interrupt_loop(int fd, std::vector& sensors, PubMaster& pm) { } int num_events = err / sizeof(*evdata); + uint64_t offset = nanos_since_epoch() - nanos_since_boot(); uint64_t ts = evdata[num_events - 1].timestamp - offset; - MessageBuilder msg; - auto orphanage = msg.getOrphanage(); - std::vector> collected_events; - collected_events.reserve(sensors.size()); - for (Sensor *sensor : sensors) { - auto orphan = orphanage.newOrphan(); - auto event = orphan.get(); - if (!sensor->get_event(event)) { + MessageBuilder msg; + if (!sensor->get_event(msg, ts)) { continue; } - event.setTimestamp(ts); - collected_events.push_back(kj::mv(orphan)); - } - - if (collected_events.size() == 0) { - continue; - } - - auto events = msg.initEvent().initSensorEvents(collected_events.size()); - for (int i = 0; i < collected_events.size(); ++i) { - events.adoptWithCaveats(i, kj::mv(collected_events[i])); - } + if (!sensor->is_data_valid(init_ts, ts)) { + continue; + } - if (ts - init_ts < init_delay) { - continue; + pm_int.send(sensor_service[sensor].c_str(), msg); } - - std::lock_guard lock(pm_mutex); - pm.send("sensorEvents", msg); } // poweroff sensors, disable interrupts @@ -106,16 +89,7 @@ void interrupt_loop(int fd, std::vector& sensors, PubMaster& pm) { } } -int sensor_loop() { - I2CBus *i2c_bus_imu; - - try { - i2c_bus_imu = new I2CBus(I2C_BUS_IMU); - } catch (std::exception &e) { - LOGE("I2CBus init failed"); - return -1; - } - +int sensor_loop(I2CBus *i2c_bus_imu) { BMX055_Accel bmx055_accel(i2c_bus_imu); BMX055_Gyro bmx055_gyro(i2c_bus_imu); BMX055_Magn bmx055_magn(i2c_bus_imu); @@ -129,6 +103,20 @@ int sensor_loop() { LightSensor light("/sys/class/i2c-adapter/i2c-2/2-0038/iio:device1/in_intensity_both_raw"); + std::map sensor_service = { + {&bmx055_accel, "accelerometer2"}, + {&bmx055_gyro, "gyroscope2"}, + {&bmx055_magn, "magnetometer"}, + {&bmx055_temp, "temperatureSensor"}, + + {&lsm6ds3_accel, "accelerometer"}, + {&lsm6ds3_gyro, "gyroscope"}, + {&lsm6ds3_temp, "temperatureSensor"}, + + {&mmc5603nj_magn, "magnetometer"}, + {&light, "lightSensor"} + }; + // Sensor init std::vector> sensors_init; // Sensor, required sensors_init.push_back({&bmx055_accel, false}); @@ -148,75 +136,79 @@ int sensor_loop() { // Initialize sensors std::vector sensors; - for (auto &sensor : sensors_init) { - int err = sensor.first->init(); + for (auto &[sensor, required] : sensors_init) { + int err = sensor->init(); if (err < 0) { - // Fail on required sensors - if (sensor.second) { + if (required) { LOGE("Error initializing sensors"); - delete i2c_bus_imu; return -1; } } else { - if (sensor.first == &bmx055_magn || sensor.first == &mmc5603nj_magn) { + + if (sensor == &bmx055_magn || sensor == &mmc5603nj_magn) { has_magnetometer = true; } - if (!sensor.first->has_interrupt_enabled()) { - sensors.push_back(sensor.first); + if (!sensor->has_interrupt_enabled()) { + sensors.push_back(sensor); } } } if (!has_magnetometer) { LOGE("No magnetometer present"); - delete i2c_bus_imu; return -1; } - PubMaster pm({"sensorEvents"}); + // 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'"); + + PubMaster pm_non_int({"gyroscope2", "accelerometer2", "temperatureSensor", + "lightSensor", "magnetometer"}); init_ts = nanos_since_boot(); // thread for reading events via interrupts std::vector lsm_interrupt_sensors = {&lsm6ds3_accel, &lsm6ds3_gyro}; - std::thread lsm_interrupt_thread(&interrupt_loop, lsm6ds3_accel.gpio_fd, std::ref(lsm_interrupt_sensors), std::ref(pm)); + std::thread lsm_interrupt_thread(&interrupt_loop, std::ref(lsm_interrupt_sensors), + std::ref(sensor_service)); // polling loop for non interrupt handled sensors while (!do_exit) { std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now(); - const int num_events = sensors.size(); - MessageBuilder msg; - auto sensor_events = msg.initEvent().initSensorEvents(num_events); - - for (int i = 0; i < num_events; i++) { - auto event = sensor_events[i]; - sensors[i]->get_event(event); - } + for (Sensor *sensor : sensors) { + MessageBuilder msg; + if (!sensor->get_event(msg)) { + continue; + } - if (nanos_since_boot() - init_ts < init_delay) { - continue; - } + if (!sensor->is_data_valid(init_ts, nanos_since_boot())) { + continue; + } - { - std::lock_guard lock(pm_mutex); - pm.send("sensorEvents", msg); + pm_non_int.send(sensor_service[sensor].c_str(), msg); } std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); std::this_thread::sleep_for(std::chrono::milliseconds(10) - (end - begin)); } - for (Sensor *sensor : sensors) { + for (Sensor *sensor : sensors) { sensor->shutdown(); } lsm_interrupt_thread.join(); - delete i2c_bus_imu; return 0; } int main(int argc, char *argv[]) { - setpriority(PRIO_PROCESS, 0, -18); - return sensor_loop(); + try { + auto i2c_bus_imu = std::make_unique(I2C_BUS_IMU); + return sensor_loop(i2c_bus_imu.get()); + } catch (std::exception &e) { + LOGE("I2CBus init failed"); + return -1; + } } diff --git a/selfdrive/sensord/tests/test_sensord.py b/selfdrive/sensord/tests/test_sensord.py index 0b5f054d2e..82bd28446b 100755 --- a/selfdrive/sensord/tests/test_sensord.py +++ b/selfdrive/sensord/tests/test_sensord.py @@ -3,55 +3,47 @@ import os import time import unittest import numpy as np -from collections import namedtuple +from collections import namedtuple, defaultdict import cereal.messaging as messaging from cereal import log from system.hardware import TICI, HARDWARE from selfdrive.manager.process_config import managed_processes +BMX = { + ('bmx055', 'acceleration'), + ('bmx055', 'gyroUncalibrated'), + ('bmx055', 'magneticUncalibrated'), + ('bmx055', 'temperature'), +} + +LSM = { + ('lsm6ds3', 'acceleration'), + ('lsm6ds3', 'gyroUncalibrated'), + ('lsm6ds3', 'temperature'), +} +LSM_C = {(x[0]+'trc', x[1]) for x in LSM} + +MMC = { + ('mmc5603nj', 'magneticUncalibrated'), +} + +RPR = { + ('rpr0521', 'light'), +} + SENSOR_CONFIGURATIONS = ( - { - ('bmx055', 'acceleration'), - ('bmx055', 'gyroUncalibrated'), - ('bmx055', 'magneticUncalibrated'), - ('bmx055', 'temperature'), - ('lsm6ds3', 'acceleration'), - ('lsm6ds3', 'gyroUncalibrated'), - ('lsm6ds3', 'temperature'), - ('rpr0521', 'light'), - }, - { - ('lsm6ds3', 'acceleration'), - ('lsm6ds3', 'gyroUncalibrated'), - ('lsm6ds3', 'temperature'), - ('mmc5603nj', 'magneticUncalibrated'), - ('rpr0521', 'light'), - }, - { - ('bmx055', 'acceleration'), - ('bmx055', 'gyroUncalibrated'), - ('bmx055', 'magneticUncalibrated'), - ('bmx055', 'temperature'), - ('lsm6ds3trc', 'acceleration'), - ('lsm6ds3trc', 'gyroUncalibrated'), - ('lsm6ds3trc', 'temperature'), - ('rpr0521', 'light'), - }, - { - ('lsm6ds3trc', 'acceleration'), - ('lsm6ds3trc', 'gyroUncalibrated'), - ('lsm6ds3trc', 'temperature'), - ('mmc5603nj', 'magneticUncalibrated'), - ('rpr0521', 'light'), - }, + (BMX | LSM | RPR), + (MMC | LSM | RPR), + (BMX | LSM_C | RPR), + (MMC| LSM_C | RPR), ) Sensor = log.SensorEventData.SensorSource SensorConfig = namedtuple('SensorConfig', ['type', 'sanity_min', 'sanity_max']) ALL_SENSORS = { Sensor.rpr0521: { - SensorConfig("light", 0, 150), + SensorConfig("light", 0, 1023), }, Sensor.lsm6ds3: { @@ -78,31 +70,30 @@ ALL_SENSORS = { } } -SENSOR_BUS = 1 -I2C_ADDR_LSM = 0x6A -LSM_INT_GPIO = 84 +LSM_IRQ = 336 + +def get_irq_count(irq: int): + with open(f"/sys/kernel/irq/{irq}/per_cpu_count") as f: + per_cpu = map(int, f.read().split(",")) + return sum(per_cpu) def read_sensor_events(duration_sec): - sensor_events = messaging.sub_sock("sensorEvents", timeout=0.1) + sensor_types = ['accelerometer', 'gyroscope', 'magnetometer', 'accelerometer2', + 'gyroscope2', 'lightSensor', 'temperatureSensor'] + esocks = {} + events = defaultdict(list) + for stype in sensor_types: + esocks[stype] = messaging.sub_sock(stype, timeout=0.1) + start_time_sec = time.monotonic() - events = [] while time.monotonic() - start_time_sec < duration_sec: - events += messaging.drain_sock(sensor_events) - time.sleep(0.01) - - assert len(events) != 0, "No sensor events collected" - return events - -def get_proc_interrupts(int_pin): - with open("/proc/interrupts") as f: - lines = f.read().split("\n") + for esock in esocks: + events[esock] += messaging.drain_sock(esocks[esock]) + time.sleep(0.1) - for line in lines: - if f" {int_pin} " in line: - return ''.join(list(filter(lambda e: e != '', line.split(' ')))[1:6]) - - return "" + assert sum(map(len, events.values())) != 0, "No sensor events collected!" + return events class TestSensord(unittest.TestCase): @classmethod @@ -115,11 +106,14 @@ class TestSensord(unittest.TestCase): # read initial sensor values every test case can use os.system("pkill -f ./_sensord") - cls.sample_secs = 5 - managed_processes["sensord"].start() - time.sleep(2) - cls.events = read_sensor_events(cls.sample_secs) - managed_processes["sensord"].stop() + try: + managed_processes["sensord"].start() + time.sleep(3) + cls.sample_secs = 10 + cls.events = read_sensor_events(cls.sample_secs) + finally: + # teardown won't run if this doesn't succeed + managed_processes["sensord"].stop() @classmethod def tearDownClass(cls): @@ -132,12 +126,10 @@ class TestSensord(unittest.TestCase): # verify correct sensors configuration seen = set() - for event in self.events: - for measurement in event.sensorEvents: - # filter unset events (bmx magn) - if measurement.version == 0: - continue - seen.add((str(measurement.source), measurement.which())) + for etype in self.events: + for measurement in self.events[etype]: + m = getattr(measurement, measurement.which()) + seen.add((str(m.source), m.which())) self.assertIn(seen, SENSOR_CONFIGURATIONS) @@ -148,10 +140,14 @@ class TestSensord(unittest.TestCase): 1: [], # accel 5: [], # gyro } - for event in self.events: - for measurement in event.sensorEvents: - if str(measurement.source).startswith("lsm6ds3") and measurement.sensor in sensor_t: - sensor_t[measurement.sensor].append(measurement.timestamp) + + for measurement in self.events['accelerometer']: + m = getattr(measurement, measurement.which()) + sensor_t[m.sensor].append(m.timestamp) + + for measurement in self.events['gyroscope']: + m = getattr(measurement, measurement.which()) + sensor_t[m.sensor].append(m.timestamp) for s, vals in sensor_t.items(): with self.subTest(sensor=s): @@ -161,9 +157,9 @@ class TestSensord(unittest.TestCase): high_delay_diffs = list(filter(lambda d: d >= 20., tdiffs)) assert len(high_delay_diffs) < 15, f"Too many large diffs: {high_delay_diffs}" - # 100-108Hz, expected 104Hz avg_diff = sum(tdiffs)/len(tdiffs) - assert 9.3 < avg_diff < 10., f"avg difference {avg_diff}, below threshold" + avg_freq = 1. / (avg_diff * 1e-3) + assert 92. < avg_freq < 114., f"avg freq {avg_freq}Hz wrong, expected 104Hz" stddev = np.std(tdiffs) assert stddev < 2.0, f"Standard-dev to big {stddev}" @@ -172,17 +168,14 @@ class TestSensord(unittest.TestCase): # verify if all sensors produce events sensor_events = dict() - for event in self.events: - for measurement in event.sensorEvents: - - # filter unset events (bmx magn) - if measurement.version == 0: - continue + for etype in self.events: + for measurement in self.events[etype]: + m = getattr(measurement, measurement.which()) - if measurement.type in sensor_events: - sensor_events[measurement.type] += 1 + if m.type in sensor_events: + sensor_events[m.type] += 1 else: - sensor_events[measurement.type] = 1 + sensor_events[m.type] = 1 for s in sensor_events: err_msg = f"Sensor {s}: 200 < {sensor_events[s]}" @@ -192,43 +185,36 @@ class TestSensord(unittest.TestCase): # ensure diff between the message logMonotime and sample timestamp is small tdiffs = list() - for event in self.events: - for measurement in event.sensorEvents: - - # filter unset events (bmx magn) - if measurement.version == 0: - continue + for etype in self.events: + for measurement in self.events[etype]: + m = getattr(measurement, measurement.which()) # check if gyro and accel timestamps are before logMonoTime - if str(measurement.source).startswith("lsm6ds3"): - if measurement.which() != 'temperature': - err_msg = f"Timestamp after logMonoTime: {measurement.timestamp} > {event.logMonoTime}" - assert measurement.timestamp < event.logMonoTime, err_msg + if str(m.source).startswith("lsm6ds3") and m.which() != 'temperature': + err_msg = f"Timestamp after logMonoTime: {m.timestamp} > {measurement.logMonoTime}" + assert m.timestamp < measurement.logMonoTime, err_msg # negative values might occur, as non interrupt packages created # before the sensor is read - tdiffs.append(abs(event.logMonoTime - measurement.timestamp)) + tdiffs.append(abs(measurement.logMonoTime - m.timestamp) / 1e6) - high_delay_diffs = set(filter(lambda d: d >= 10*10**6, tdiffs)) - assert len(high_delay_diffs) < 15, f"Too many high delay packages: {high_delay_diffs}" + high_delay_diffs = set(filter(lambda d: d >= 15., tdiffs)) + assert len(high_delay_diffs) < 20, f"Too many measurements published : {high_delay_diffs}" avg_diff = round(sum(tdiffs)/len(tdiffs), 4) - assert avg_diff < 4*10**6, f"Avg packet diff: {avg_diff:.1f}ns" + assert avg_diff < 4, f"Avg packet diff: {avg_diff:.1f}ms" stddev = np.std(tdiffs) - assert stddev < 2*10**6, f"Timing diffs have to high stddev: {stddev}" + assert stddev < 2, f"Timing diffs have too high stddev: {stddev}" def test_sensor_values_sanity_check(self): sensor_values = dict() - for event in self.events: - for m in event.sensorEvents: - - # filter unset events (bmx magn) - if m.version == 0: - continue - + for etype in self.events: + for measurement in self.events[etype]: + m = getattr(measurement, measurement.which()) key = (m.source.raw, m.which()) values = getattr(m, m.which()) + if hasattr(values, 'v'): values = values.v values = np.atleast_1d(values) @@ -240,7 +226,6 @@ class TestSensord(unittest.TestCase): # Sanity check sensor values and counts for sensor, stype in sensor_values: - for s in ALL_SENSORS[sensor]: if s.type != stype: continue @@ -256,24 +241,24 @@ class TestSensord(unittest.TestCase): assert s.sanity_min <= mean_norm <= s.sanity_max, err_msg def test_sensor_verify_no_interrupts_after_stop(self): - managed_processes["sensord"].start() time.sleep(3) # read /proc/interrupts to verify interrupts are received - state_one = get_proc_interrupts(LSM_INT_GPIO) + state_one = get_irq_count(LSM_IRQ) time.sleep(1) - state_two = get_proc_interrupts(LSM_INT_GPIO) + state_two = get_irq_count(LSM_IRQ) - assert state_one != state_two, f"no interrupts received after sensord start!\n{state_one} {state_two}" + error_msg = f"no interrupts received after sensord start!\n{state_one} {state_two}" + assert state_one != state_two, error_msg managed_processes["sensord"].stop() time.sleep(1) # read /proc/interrupts to verify no more interrupts are received - state_one = get_proc_interrupts(LSM_INT_GPIO) + state_one = get_irq_count(LSM_IRQ) time.sleep(1) - state_two = get_proc_interrupts(LSM_INT_GPIO) + state_two = get_irq_count(LSM_IRQ) assert state_one == state_two, "Interrupts received after sensord stop!" diff --git a/selfdrive/test/longitudinal_maneuvers/plant.py b/selfdrive/test/longitudinal_maneuvers/plant.py index 21af1cd3b1..e81510e9ba 100755 --- a/selfdrive/test/longitudinal_maneuvers/plant.py +++ b/selfdrive/test/longitudinal_maneuvers/plant.py @@ -8,7 +8,7 @@ from common.realtime import Ratekeeper, DT_MDL from selfdrive.controls.lib.longcontrol import LongCtrlState from selfdrive.modeld.constants import T_IDXS from selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner - +from selfdrive.controls.lib.radar_helpers import _LEAD_ACCEL_TAU class Plant(): messaging_initialized = False @@ -83,7 +83,8 @@ class Plant(): lead.vLead = float(v_lead) lead.vLeadK = float(v_lead) lead.aLeadK = float(a_lead) - lead.aLeadTau = float(1.5) + # TODO use real radard logic for this + lead.aLeadTau = float(_LEAD_ACCEL_TAU) lead.status = status lead.modelProb = float(prob) if not self.only_lead2: diff --git a/selfdrive/test/process_replay/compare_logs.py b/selfdrive/test/process_replay/compare_logs.py index bf6daf5fed..c14956b1b2 100755 --- a/selfdrive/test/process_replay/compare_logs.py +++ b/selfdrive/test/process_replay/compare_logs.py @@ -41,7 +41,7 @@ def remove_ignored_fields(msg, ignore): elif isinstance(v, numbers.Number): val = 0 else: - raise NotImplementedError + raise NotImplementedError('Error ignoring field') setattr(attr, keys[-1], val) return msg.as_reader() diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index ccd89bea9a..ecfacbf5d2 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -22,7 +22,7 @@ from tools.lib.logreader import LogReader TEST_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36" SEGMENT = 0 -MAX_FRAMES = 10 if PC else 1300 +MAX_FRAMES = 100 if PC else 1300 SEND_EXTRA_INPUTS = bool(os.getenv("SEND_EXTRA_INPUTS", "0")) @@ -174,7 +174,7 @@ if __name__ == "__main__": 'driverStateV2.dspExecutionTime' ] # TODO this tolerance is absurdly large - tolerance = 5e-1 if PC else None + tolerance = 2.0 if PC else None results: Any = {TEST_ROUTE: {}} log_paths: Any = {TEST_ROUTE: {"models": {'ref': BASE_URL + log_fn, 'new': log_fn}}} results[TEST_ROUTE]["models"] = compare_logs(cmp_log, log_msgs, tolerance=tolerance, ignore_fields=ignore) diff --git a/selfdrive/test/process_replay/model_replay_ref_commit b/selfdrive/test/process_replay/model_replay_ref_commit index 958d3da14d..b3e9c8c488 100644 --- a/selfdrive/test/process_replay/model_replay_ref_commit +++ b/selfdrive/test/process_replay/model_replay_ref_commit @@ -1 +1 @@ -c40319a454840d8a2196ec1227d27b536ee14375 +bfb0a2a52212d2aa1619d999aaae97fa7f7ff788 diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index 038b0cf468..9d37be4a56 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -328,7 +328,8 @@ CONFIGS = [ proc_name="locationd", pub_sub={ "cameraOdometry": ["liveLocationKalman"], - "sensorEvents": [], "gpsLocationExternal": [], "liveCalibration": [], "carState": [], + "accelerometer": [], "gyroscope": [], + "gpsLocationExternal": [], "liveCalibration": [], "carState": [], }, ignore=["logMonoTime", "valid"], init_callback=get_car_params, @@ -416,6 +417,7 @@ def setup_env(simulation=False, CP=None, cfg=None, controlsState=None): params.put_bool("DisengageOnAccelerator", True) params.put_bool("WideCameraOnly", False) params.put_bool("DisableLogging", False) + params.put_bool("UbloxAvailable", True) os.environ["NO_RADAR_SLEEP"] = "1" os.environ["REPLAY"] = "1" diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 7aad2b291d..864b7019a0 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -051fa5bea42027c1a756ae61fd0c752c1e911899 \ No newline at end of file +e5a86c14e2318f2dd218b3985cdbea6f875f7d83 diff --git a/selfdrive/test/process_replay/regen.py b/selfdrive/test/process_replay/regen.py index d565e36390..c9f9c6c362 100755 --- a/selfdrive/test/process_replay/regen.py +++ b/selfdrive/test/process_replay/regen.py @@ -30,10 +30,11 @@ def replay_panda_states(s, msgs): rk = Ratekeeper(service_list[s].frequency, print_delay_threshold=None) smsgs = [m for m in msgs if m.which() in ['pandaStates', 'pandaStateDEPRECATED']] - # TODO: new safety params from flags, remove after getting new routes for Toyota + # TODO: safety param migration should be handled automatically safety_param_migration = { "TOYOTA PRIUS 2017": EPS_SCALE["TOYOTA PRIUS 2017"] | Panda.FLAG_TOYOTA_STOCK_LONGITUDINAL, "TOYOTA RAV4 2017": EPS_SCALE["TOYOTA RAV4 2017"] | Panda.FLAG_TOYOTA_ALT_BRAKE, + "KIA EV6 2022": Panda.FLAG_HYUNDAI_EV_GAS | Panda.FLAG_HYUNDAI_CANFD_HDA2, } # Migrate safety param base on carState @@ -56,6 +57,7 @@ def replay_panda_states(s, msgs): pm.send(s, new_m) else: new_m = m.as_builder() + new_m.pandaStates[-1].safetyParam = safety_param new_m.logMonoTime = int(sec_since_boot() * 1e9) pm.send(s, new_m) @@ -90,19 +92,16 @@ def replay_device_state(s, msgs): rk.keep_time() -def replay_sensor_events(s, msgs): +def replay_sensor_event(s, msgs): pm = messaging.PubMaster([s, ]) rk = Ratekeeper(service_list[s].frequency, print_delay_threshold=None) smsgs = [m for m in msgs if m.which() == s] while True: for m in smsgs: - new_m = m.as_builder() - new_m.logMonoTime = int(sec_since_boot() * 1e9) - - for evt in new_m.sensorEvents: - evt.timestamp = new_m.logMonoTime - - pm.send(s, new_m) + m = m.as_builder() + m.logMonoTime = int(sec_since_boot() * 1e9) + getattr(m, m.which()).timestamp = m.logMonoTime + pm.send(m.which(), m) rk.keep_time() @@ -193,8 +192,49 @@ def migrate_carparams(lr): return all_msgs +def migrate_sensorEvents(lr, old_logtime=False): + all_msgs = [] + for msg in lr: + if msg.which() != 'sensorEventsDEPRECATED': + all_msgs.append(msg) + continue + + # migrate to split sensor events + for evt in msg.sensorEventsDEPRECATED: + # build new message for each sensor type + sensor_service = '' + if evt.which() == 'acceleration': + sensor_service = 'accelerometer' + elif evt.which() == 'gyro' or evt.which() == 'gyroUncalibrated': + sensor_service = 'gyroscope' + elif evt.which() == 'light' or evt.which() == 'proximity': + sensor_service = 'lightSensor' + elif evt.which() == 'magnetic' or evt.which() == 'magneticUncalibrated': + sensor_service = 'magnetometer' + elif evt.which() == 'temperature': + sensor_service = 'temperatureSensor' + + m = messaging.new_message(sensor_service) + m.valid = True + if old_logtime: + m.logMonoTime = msg.logMonoTime + + m_dat = getattr(m, sensor_service) + m_dat.version = evt.version + m_dat.sensor = evt.sensor + m_dat.type = evt.type + m_dat.source = evt.source + if old_logtime: + m_dat.timestamp = evt.timestamp + setattr(m_dat, evt.which(), getattr(evt, evt.which())) + + all_msgs.append(m.as_reader()) + + return all_msgs + def regen_segment(lr, frs=None, outdir=FAKEDATA, disable_tqdm=False): lr = migrate_carparams(list(lr)) + lr = migrate_sensorEvents(list(lr)) if frs is None: frs = dict() @@ -212,7 +252,9 @@ def regen_segment(lr, frs=None, outdir=FAKEDATA, disable_tqdm=False): vs, cam_procs = replay_cameras(lr, frs, disable_tqdm=disable_tqdm) fake_daemons = { 'sensord': [ - multiprocessing.Process(target=replay_sensor_events, args=('sensorEvents', lr)), + multiprocessing.Process(target=replay_sensor_event, args=('accelerometer', lr)), + multiprocessing.Process(target=replay_sensor_event, args=('gyroscope', lr)), + multiprocessing.Process(target=replay_sensor_event, args=('magnetometer', lr)), ], 'pandad': [ multiprocessing.Process(target=replay_service, args=('can', lr)), diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index ee892a2fd9..683387dce8 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -18,7 +18,7 @@ from tools.lib.logreader import LogReader source_segments = [ ("BODY", "937ccb7243511b65|2022-05-24--16-03-09--1"), # COMMA.BODY ("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.SONATA - ("HYUNDAI", "d824e27e8c60172c|2022-09-13--11-26-50--2"), # HYUNDAI.KIA_EV6 + ("HYUNDAI2", "d824e27e8c60172c|2022-09-13--11-26-50--2"), # HYUNDAI.KIA_EV6 ("TOYOTA", "0982d79ebb0de295|2021-01-04--17-13-21--13"), # TOYOTA.PRIUS (INDI) ("TOYOTA2", "0982d79ebb0de295|2021-01-03--20-03-36--6"), # TOYOTA.RAV4 (LQR) ("TOYOTA3", "f7d7e3538cda1a2a|2021-08-16--08-55-34--6"), # TOYOTA.COROLLA_TSS2 @@ -38,21 +38,21 @@ source_segments = [ ] segments = [ - ("BODY", "regen9D38397D30D|2022-09-09--13-12-48--0"), - ("HYUNDAI", "regenB3953B393C0|2022-09-09--14-49-37--0"), - ("HYUNDAI", "regen8DB830E5376|2022-09-13--17-24-37--0"), - ("TOYOTA", "regen8FCBB6F06F1|2022-09-09--13-14-07--0"), - ("TOYOTA2", "regen956BFA75300|2022-09-09--14-51-24--0"), - ("TOYOTA3", "regenE909BC2F430|2022-09-09--20-44-49--0"), - ("HONDA", "regenD1D10209015|2022-09-09--14-53-09--0"), - ("HONDA2", "regen3F7C2EFDC08|2022-09-09--19-41-19--0"), - ("CHRYSLER", "regen92783EAE66B|2022-09-09--13-15-44--0"), - ("RAM", "regenBE5DAAEF30F|2022-09-13--17-06-24--0"), - ("SUBARU", "regen8A363AF7E14|2022-09-13--17-20-39--0"), - ("GM", "regen31EA3F9A37C|2022-09-09--21-06-36--0"), - ("NISSAN", "regenAA21ADE5921|2022-09-09--19-44-37--0"), - ("VOLKSWAGEN", "regenA1BF4D17761|2022-09-09--19-46-24--0"), - ("MAZDA", "regen1994C97E977|2022-09-13--16-34-44--0"), + ("BODY", "regenFA002A80700|2022-09-27--15-37-02--0"), + ("HYUNDAI", "regenBE53A59065B|2022-09-27--16-52-03--0"), + ("HYUNDAI2", "regenFA8B5CA9840|2022-10-12--21-47-06--0"), + ("TOYOTA", "regen929C5790007|2022-09-27--16-27-47--0"), + ("TOYOTA2", "regenEA3950D7F22|2022-09-27--15-43-24--0"), + ("TOYOTA3", "regen89026F6BD8D|2022-09-27--15-45-37--0"), + ("HONDA", "regenC7D5645EB17|2022-09-27--15-47-29--0"), + ("HONDA2", "regenCC2ECCE5742|2022-09-27--16-18-01--0"), + ("CHRYSLER", "regenC253C4DAC90|2022-09-27--15-51-03--0"), + ("RAM", "regen20490083AE7|2022-09-27--15-53-15--0"), + ("SUBARU", "regen1E72BBDCED5|2022-09-27--15-55-31--0"), + ("GM", "regen45B05A80EF6|2022-09-27--15-57-22--0"), + ("NISSAN", "regenC19D899B46D|2022-09-27--15-59-13--0"), + ("VOLKSWAGEN", "regenD8F7AC4BD0D|2022-09-27--16-41-45--0"), + ("MAZDA", "regenFC3F9ECBB64|2022-09-27--16-03-09--0"), ] # dashcamOnly makes don't need to be tested until a full port is done diff --git a/selfdrive/test/profiling/profiler.py b/selfdrive/test/profiling/profiler.py index 91226fc577..732a69eebd 100755 --- a/selfdrive/test/profiling/profiler.py +++ b/selfdrive/test/profiling/profiler.py @@ -53,6 +53,7 @@ def profile(proc, func, car='toyota'): msgs = list(LogReader(rlog_url)) * int(os.getenv("LOOP", "1")) os.environ['FINGERPRINT'] = fingerprint + os.environ['SKIP_FW_QUERY'] = "1" os.environ['REPLAY'] = "1" def run(sm, pm, can_sock): diff --git a/selfdrive/test/setup_device_ci.sh b/selfdrive/test/setup_device_ci.sh index bf2f93e1c3..a9c6527970 100755 --- a/selfdrive/test/setup_device_ci.sh +++ b/selfdrive/test/setup_device_ci.sh @@ -59,9 +59,9 @@ git fetch --verbose origin $GIT_COMMIT find . -maxdepth 1 -not -path './.git' -not -name '.' -not -name '..' -exec rm -rf '{}' \; git reset --hard $GIT_COMMIT git checkout $GIT_COMMIT -git clean -xdf +git clean -xdff git submodule update --init --recursive -git submodule foreach --recursive "git reset --hard && git clean -xdf" +git submodule foreach --recursive "git reset --hard && git clean -xdff" git lfs pull (ulimit -n 65535 && git lfs prune) diff --git a/selfdrive/test/update_ci_routes.py b/selfdrive/test/update_ci_routes.py index a1e8c35f6b..201ffb745a 100755 --- a/selfdrive/test/update_ci_routes.py +++ b/selfdrive/test/update_ci_routes.py @@ -14,11 +14,16 @@ SOURCES = [ (_DATA_ACCOUNT_CI, "commadataci"), ] -DEST_KEY = azureutil.get_user_token(_DATA_ACCOUNT_CI, "openpilotci") -SOURCE_KEYS = [azureutil.get_user_token(account, bucket) for account, bucket in SOURCES] -SERVICE = BlockBlobService(_DATA_ACCOUNT_CI, sas_token=DEST_KEY) + +def get_azure_keys(): + dest_key = azureutil.get_user_token(_DATA_ACCOUNT_CI, "openpilotci") + source_keys = [azureutil.get_user_token(account, bucket) for account, bucket in SOURCES] + service = BlockBlobService(_DATA_ACCOUNT_CI, sas_token=dest_key) + return dest_key, source_keys, service + def upload_route(path, exclude_patterns=None): + dest_key, _, _ = get_azure_keys() if exclude_patterns is None: exclude_patterns = ['*/dcamera.hevc'] @@ -29,27 +34,28 @@ def upload_route(path, exclude_patterns=None): "azcopy", "copy", f"{path}/*", - f"https://{_DATA_ACCOUNT_CI}.blob.core.windows.net/openpilotci/{destpath}?{DEST_KEY}", + f"https://{_DATA_ACCOUNT_CI}.blob.core.windows.net/openpilotci/{destpath}?{dest_key}", "--recursive=false", "--overwrite=false", ] + [f"--exclude-pattern={p}" for p in exclude_patterns] subprocess.check_call(cmd) def sync_to_ci_public(route): + dest_key, source_keys, service = get_azure_keys() key_prefix = route.replace('|', '/') dongle_id = key_prefix.split('/')[0] - if next(azureutil.list_all_blobs(SERVICE, "openpilotci", prefix=key_prefix), None) is not None: + if next(azureutil.list_all_blobs(service, "openpilotci", prefix=key_prefix), None) is not None: return True print(f"Uploading {route}") - for (source_account, source_bucket), source_key in zip(SOURCES, SOURCE_KEYS): + for (source_account, source_bucket), source_key in zip(SOURCES, source_keys): print(f"Trying {source_account}/{source_bucket}") cmd = [ "azcopy", "copy", f"https://{source_account}.blob.core.windows.net/{source_bucket}/{key_prefix}?{source_key}", - f"https://{_DATA_ACCOUNT_CI}.blob.core.windows.net/openpilotci/{dongle_id}?{DEST_KEY}", + f"https://{_DATA_ACCOUNT_CI}.blob.core.windows.net/openpilotci/{dongle_id}?{dest_key}", "--recursive=true", "--overwrite=false", "--exclude-pattern=*/dcamera.hevc", diff --git a/selfdrive/thermald/thermald.py b/selfdrive/thermald/thermald.py index 5c2fbd6825..89b81f06ec 100755 --- a/selfdrive/thermald/thermald.py +++ b/selfdrive/thermald/thermald.py @@ -177,7 +177,8 @@ def thermald_thread(end_event, hw_queue): modem_temps=[], ) - temp_filter = FirstOrderFilter(0., TEMP_TAU, DT_TRML) + all_temp_filter = FirstOrderFilter(0., TEMP_TAU, DT_TRML) + offroad_temp_filter = FirstOrderFilter(0., TEMP_TAU, DT_TRML) should_start_prev = False in_car = False engaged_prev = False @@ -239,24 +240,32 @@ def thermald_thread(end_event, hw_queue): msg.deviceState.screenBrightnessPercent = HARDWARE.get_screen_brightness() - max_comp_temp = temp_filter.update( - max(max(msg.deviceState.cpuTempC), msg.deviceState.memoryTempC, max(msg.deviceState.gpuTempC)) - ) + # this one is only used for offroad + temp_sources = [ + msg.deviceState.memoryTempC, + max(msg.deviceState.cpuTempC), + max(msg.deviceState.gpuTempC), + ] + offroad_comp_temp = offroad_temp_filter.update(max(temp_sources)) + + # this drives the thermal status while onroad + temp_sources.append(max(msg.deviceState.pmicTempC)) + all_comp_temp = all_temp_filter.update(max(temp_sources)) if fan_controller is not None: - msg.deviceState.fanSpeedPercentDesired = fan_controller.update(max_comp_temp, onroad_conditions["ignition"]) + msg.deviceState.fanSpeedPercentDesired = fan_controller.update(all_comp_temp, onroad_conditions["ignition"]) is_offroad_for_5_min = (started_ts is None) and ((not started_seen) or (off_ts is None) or (sec_since_boot() - off_ts > 60 * 5)) - if is_offroad_for_5_min and max_comp_temp > OFFROAD_DANGER_TEMP: + if is_offroad_for_5_min and offroad_comp_temp > OFFROAD_DANGER_TEMP: # If device is offroad we want to cool down before going onroad # since going onroad increases load and can make temps go over 107 thermal_status = ThermalStatus.danger else: current_band = THERMAL_BANDS[thermal_status] band_idx = list(THERMAL_BANDS.keys()).index(thermal_status) - if current_band.min_temp is not None and max_comp_temp < current_band.min_temp: + if current_band.min_temp is not None and all_comp_temp < current_band.min_temp: thermal_status = list(THERMAL_BANDS.keys())[band_idx - 1] - elif current_band.max_temp is not None and max_comp_temp > current_band.max_temp: + elif current_band.max_temp is not None and all_comp_temp > current_band.max_temp: thermal_status = list(THERMAL_BANDS.keys())[band_idx + 1] # **** starting logic **** @@ -269,6 +278,7 @@ def thermald_thread(end_event, hw_queue): startup_conditions["up_to_date"] = params.get("Offroad_ConnectivityNeeded") is None or params.get_bool("DisableUpdates") or params.get_bool("SnoozeUpdate") startup_conditions["not_uninstalling"] = not params.get_bool("DoUninstall") startup_conditions["accepted_terms"] = params.get("HasAcceptedTerms") == terms_version + startup_conditions["offroad_min_time"] = (not started_seen) or ((off_ts is not None) and (sec_since_boot() - off_ts) > 5.) # with 2% left, we killall, otherwise the phone will take a long time to boot startup_conditions["free_space"] = msg.deviceState.freeSpacePercent > 2 @@ -329,7 +339,8 @@ def thermald_thread(end_event, hw_queue): started_seen = True else: if onroad_conditions["ignition"] and (startup_conditions != startup_conditions_prev): - cloudlog.event("Startup blocked", startup_conditions=startup_conditions, onroad_conditions=onroad_conditions) + cloudlog.event("Startup blocked", startup_conditions=startup_conditions, onroad_conditions=onroad_conditions, error=True) + startup_conditions_prev = startup_conditions.copy() started_ts = None if off_ts is None: @@ -363,7 +374,6 @@ def thermald_thread(end_event, hw_queue): pm.send("deviceState", msg) should_start_prev = should_start - startup_conditions_prev = startup_conditions.copy() # Log to statsd statlog.gauge("free_space_percent", msg.deviceState.freeSpacePercent) diff --git a/selfdrive/tombstoned.py b/selfdrive/tombstoned.py index 0045e0766c..61a575f141 100755 --- a/selfdrive/tombstoned.py +++ b/selfdrive/tombstoned.py @@ -62,47 +62,6 @@ def get_tombstones(): return files -def report_tombstone_android(fn): - f_size = os.path.getsize(fn) - if f_size > MAX_SIZE: - cloudlog.error(f"Tombstone {fn} too big, {f_size}. Skipping...") - return - - with open(fn, encoding='ISO-8859-1') as f: - contents = f.read() - - message = " ".join(contents.split('\n')[5:7]) - - # Cut off pid/tid, since that varies per run - name_idx = message.find('name') - if name_idx >= 0: - message = message[name_idx:] - - executable = "" - start_exe_idx = message.find('>>> ') - end_exe_idx = message.find(' <<<') - if start_exe_idx >= 0 and end_exe_idx >= 0: - executable = message[start_exe_idx + 4:end_exe_idx] - - # Cut off fault addr - fault_idx = message.find(', fault addr') - if fault_idx >= 0: - message = message[:fault_idx] - - sentry.report_tombstone(fn, message, contents) - - # Copy crashlog to upload folder - clean_path = executable.replace('./', '').replace('/', '_') - date = datetime.datetime.now().strftime("%Y-%m-%d--%H-%M-%S") - - new_fn = f"{date}_{get_commit(default='nocommit')[:8]}_{safe_fn(clean_path)}"[:MAX_TOMBSTONE_FN_LEN] - - crashlog_dir = os.path.join(ROOT, "crash") - mkdirs_exists_ok(crashlog_dir) - - shutil.copy(fn, os.path.join(crashlog_dir, new_fn)) - - def report_tombstone_apport(fn): f_size = os.path.getsize(fn) if f_size > MAX_SIZE: @@ -199,7 +158,7 @@ def main() -> NoReturn: if fn.endswith(".crash"): report_tombstone_apport(fn) else: - report_tombstone_android(fn) + cloudlog.error(f"unknown crash type: {fn}") except Exception: cloudlog.exception(f"Error reporting tombstone {fn}") diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 92f6578dfc..84e055752a 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -32,6 +32,7 @@ if maps: qt_env['CPPDEFINES'] += ["ENABLE_MAPS"] widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs) +Export('widgets') qt_libs = [widgets, qt_util] + base_libs # build assets diff --git a/selfdrive/ui/qt/maps/map_settings.cc b/selfdrive/ui/qt/maps/map_settings.cc index dd2ad04a7d..3205ca517d 100644 --- a/selfdrive/ui/qt/maps/map_settings.cc +++ b/selfdrive/ui/qt/maps/map_settings.cc @@ -115,7 +115,9 @@ MapPanel::MapPanel(QWidget* parent) : QWidget(parent) { stack->addWidget(main_widget); stack->addWidget(no_prime_widget); - stack->setCurrentIndex(uiState()->prime_type ? 0 : 1); + connect(uiState(), &UIState::primeTypeChanged, [=](int prime_type) { + stack->setCurrentIndex(prime_type ? 0 : 1); + }); QVBoxLayout *wrapper = new QVBoxLayout(this); wrapper->addWidget(stack); @@ -194,7 +196,6 @@ void MapPanel::parseResponse(const QString &response, bool success) { } void MapPanel::refresh() { - stack->setCurrentIndex(uiState()->prime_type ? 0 : 1); if (cur_destinations == prev_destinations) return; QJsonDocument doc = QJsonDocument::fromJson(cur_destinations.trimmed().toUtf8()); diff --git a/selfdrive/ui/qt/offroad/networking.cc b/selfdrive/ui/qt/offroad/networking.cc index 7ec8691feb..13697adfb5 100644 --- a/selfdrive/ui/qt/offroad/networking.cc +++ b/selfdrive/ui/qt/offroad/networking.cc @@ -8,9 +8,11 @@ #include #include +#include "selfdrive/ui/ui.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/widgets/controls.h" +#include "selfdrive/ui/qt/widgets/prime.h" #include "selfdrive/ui/qt/widgets/scrollview.h" @@ -150,17 +152,16 @@ AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWid // Roaming toggle const bool roamingEnabled = params.getBool("GsmRoaming"); - ToggleControl *roamingToggle = new ToggleControl(tr("Enable Roaming"), "", "", roamingEnabled); - QObject::connect(roamingToggle, &SshToggle::toggleFlipped, [=](bool state) { + roamingToggle = new ToggleControl(tr("Enable Roaming"), "", "", roamingEnabled); + QObject::connect(roamingToggle, &ToggleControl::toggleFlipped, [=](bool state) { params.putBool("GsmRoaming", state); - wifi->updateGsmSettings(state, QString::fromStdString(params.get("GsmApn"))); + wifi->updateGsmSettings(state, QString::fromStdString(params.get("GsmApn")), params.getBool("GsmMetered")); }); list->addItem(roamingToggle); // APN settings - ButtonControl *editApnButton = new ButtonControl(tr("APN Setting"), tr("EDIT")); + editApnButton = new ButtonControl(tr("APN Setting"), tr("EDIT")); connect(editApnButton, &ButtonControl::clicked, [=]() { - const bool roamingEnabled = params.getBool("GsmRoaming"); const QString cur_apn = QString::fromStdString(params.get("GsmApn")); QString apn = InputDialog::getText(tr("Enter APN"), this, tr("leave blank for automatic configuration"), false, -1, cur_apn).trimmed(); @@ -169,12 +170,28 @@ AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWid } else { params.put("GsmApn", apn.toStdString()); } - wifi->updateGsmSettings(roamingEnabled, apn); + wifi->updateGsmSettings(params.getBool("GsmRoaming"), apn, params.getBool("GsmMetered")); }); list->addItem(editApnButton); + // Metered toggle + const bool metered = params.getBool("GsmMetered"); + meteredToggle = new ToggleControl(tr("Cellular Metered"), tr("Prevent large data uploads when on a metered connection"), "", metered); + QObject::connect(meteredToggle, &SshToggle::toggleFlipped, [=](bool state) { + params.putBool("GsmMetered", state); + wifi->updateGsmSettings(params.getBool("GsmRoaming"), QString::fromStdString(params.get("GsmApn")), state); + }); + list->addItem(meteredToggle); + // Set initial config - wifi->updateGsmSettings(roamingEnabled, QString::fromStdString(params.get("GsmApn"))); + wifi->updateGsmSettings(roamingEnabled, QString::fromStdString(params.get("GsmApn")), metered); + + connect(uiState(), &UIState::primeTypeChanged, this, [=](int prime_type) { + bool gsmVisible = prime_type == PrimeType::NONE || prime_type == PrimeType::LITE; + roamingToggle->setVisible(gsmVisible); + editApnButton->setVisible(gsmVisible); + meteredToggle->setVisible(gsmVisible); + }); main_layout->addWidget(new ScrollView(list, this)); main_layout->addStretch(1); diff --git a/selfdrive/ui/qt/offroad/networking.h b/selfdrive/ui/qt/offroad/networking.h index 4fc9a53d93..79cbcc3493 100644 --- a/selfdrive/ui/qt/offroad/networking.h +++ b/selfdrive/ui/qt/offroad/networking.h @@ -40,6 +40,9 @@ public: private: LabelControl* ipLabel; ToggleControl* tetheringToggle; + ToggleControl* roamingToggle; + ButtonControl* editApnButton; + ToggleControl* meteredToggle; WifiManager* wifi = nullptr; Params params; diff --git a/selfdrive/ui/qt/offroad/networkmanager.h b/selfdrive/ui/qt/offroad/networkmanager.h index 52d85c16af..31b33fc9f5 100644 --- a/selfdrive/ui/qt/offroad/networkmanager.h +++ b/selfdrive/ui/qt/offroad/networkmanager.h @@ -36,3 +36,10 @@ const int NM_DEVICE_TYPE_WIFI = 2; const int NM_DEVICE_TYPE_MODEM = 8; const int NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8; const int DBUS_TIMEOUT = 100; + +// https://developer-old.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NMMetered +const int NM_METERED_UNKNOWN = 0; +const int NM_METERED_YES = 1; +const int NM_METERED_NO = 2; +const int NM_METERED_GUESS_YES = 3; +const int NM_METERED_GUESS_NO = 4; diff --git a/selfdrive/ui/qt/offroad/wifiManager.cc b/selfdrive/ui/qt/offroad/wifiManager.cc index fbb64b972e..62de3041b9 100644 --- a/selfdrive/ui/qt/offroad/wifiManager.cc +++ b/selfdrive/ui/qt/offroad/wifiManager.cc @@ -345,7 +345,7 @@ NetworkType WifiManager::currentNetworkType() { return NetworkType::NONE; } -void WifiManager::updateGsmSettings(bool roaming, QString apn) { +void WifiManager::updateGsmSettings(bool roaming, QString apn, bool metered) { if (!lteConnectionPath.path().isEmpty()) { bool changes = false; bool auto_config = apn.isEmpty(); @@ -368,6 +368,13 @@ void WifiManager::updateGsmSettings(bool roaming, QString apn) { changes = true; } + int meteredInt = metered ? NM_METERED_NO : NM_METERED_UNKNOWN; + if (settings.value("connection").value("metered").toInt() != meteredInt) { + qWarning() << "Changing connection.metered to" << meteredInt; + settings["connection"]["metered"] = meteredInt; + changes = true; + } + if (changes) { call(lteConnectionPath.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "UpdateUnsaved", QVariant::fromValue(settings)); // update is temporary deactivateConnection(lteConnectionPath); @@ -408,16 +415,16 @@ void WifiManager::addTetheringConnection() { } void WifiManager::tetheringActivated(QDBusPendingCallWatcher *call) { - int prime_type = uiState()->prime_type; - int ipv4_forward = (prime_type == PrimeType::NONE || prime_type == PrimeType::LITE); - - if (!ipv4_forward) { - QTimer::singleShot(5000, this, [=] { - qWarning() << "net.ipv4.ip_forward = 0"; - std::system("sudo sysctl net.ipv4.ip_forward=0"); - }); - } - call->deleteLater(); + int prime_type = uiState()->prime_type; + int ipv4_forward = (prime_type == PrimeType::NONE || prime_type == PrimeType::LITE); + + if (!ipv4_forward) { + QTimer::singleShot(5000, this, [=] { + qWarning() << "net.ipv4.ip_forward = 0"; + std::system("sudo sysctl net.ipv4.ip_forward=0"); + }); + } + call->deleteLater(); } void WifiManager::setTetheringEnabled(bool enabled) { diff --git a/selfdrive/ui/qt/offroad/wifiManager.h b/selfdrive/ui/qt/offroad/wifiManager.h index 07b982c2c2..01f9cd6b65 100644 --- a/selfdrive/ui/qt/offroad/wifiManager.h +++ b/selfdrive/ui/qt/offroad/wifiManager.h @@ -50,7 +50,7 @@ public: bool isKnownConnection(const QString &ssid); std::optional activateWifiConnection(const QString &ssid); NetworkType currentNetworkType(); - void updateGsmSettings(bool roaming, QString apn); + void updateGsmSettings(bool roaming, QString apn, bool metered); void connect(const Network &ssid, const QString &password = {}, const QString &username = {}); // Tethering functions diff --git a/selfdrive/ui/qt/widgets/prime.cc b/selfdrive/ui/qt/widgets/prime.cc index 04684fc765..da2f4e60d1 100644 --- a/selfdrive/ui/qt/widgets/prime.cc +++ b/selfdrive/ui/qt/widgets/prime.cc @@ -312,11 +312,7 @@ void SetupWidget::replyFinished(const QString &response, bool success) { QJsonObject json = doc.object(); int prime_type = json["prime_type"].toInt(); - - if (uiState()->prime_type != prime_type) { - uiState()->prime_type = prime_type; - Params().put("PrimeType", std::to_string(prime_type)); - } + uiState()->prime_type = prime_type; if (!json["is_paired"].toBool()) { mainLayout->setCurrentIndex(0); diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 543893d440..b39c83c098 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -58,6 +58,14 @@ leave blank for automatic configuration 空白のままにして、自動設定にします + + Cellular Metered + + + + Prevent large data uploads when on a metered connection + + ConfirmationDialog diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 2a806aaed9..86cd8f990a 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -58,6 +58,14 @@ leave blank for automatic configuration 자동설정하려면 공백으로 두세요 + + Cellular Metered + + + + Prevent large data uploads when on a metered connection + + ConfirmationDialog @@ -960,11 +968,11 @@ location set openpilot longitudinal control is not currently available for this car. - + 현재 이 차량에는 openpilot 롱컨트롤을 사용할 수 없습니다. Enable experimental longitudinal control to enable this. - + openpilot 롱컨트롤을 활성화합니다. (실험적) Disengage On Accelerator Pedal diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index ff6f27dd46..8f59bf4715 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -58,6 +58,14 @@ leave blank for automatic configuration deixe em branco para configuração automática + + Cellular Metered + Plano de Dados Limitado + + + Prevent large data uploads when on a metered connection + Evite grandes uploads de dados quando estiver em uma conexão limitada + ConfirmationDialog @@ -468,7 +476,7 @@ trabalho definido PRIME FEATURES: - APRIMORAMENTOS PRIME: + BENEFÍCIOS PRIME: Remote access diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 6300875ee3..1d942387e0 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -58,6 +58,14 @@ leave blank for automatic configuration 留空以自动配置 + + Cellular Metered + + + + Prevent large data uploads when on a metered connection + + ConfirmationDialog diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 73faf11f69..816d4fd3cc 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -58,6 +58,14 @@ leave blank for automatic configuration 留空白將自動配置 + + Cellular Metered + + + + Prevent large data uploads when on a metered connection + + ConfirmationDialog diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 2722b68f17..152c7fbfcd 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -162,31 +162,8 @@ static void update_state(UIState *s) { if (sm.updated("carParams")) { scene.longitudinal_control = sm["carParams"].getCarParams().getOpenpilotLongitudinalControl(); } - if (!scene.started && sm.updated("sensorEvents")) { - for (auto sensor : sm["sensorEvents"].getSensorEvents()) { - if (sensor.which() == cereal::SensorEventData::ACCELERATION) { - auto accel = sensor.getAcceleration().getV(); - if (accel.totalSize().wordCount) { // TODO: sometimes empty lists are received. Figure out why - scene.accel_sensor = accel[2]; - } - } else if (sensor.which() == cereal::SensorEventData::GYRO_UNCALIBRATED) { - auto gyro = sensor.getGyroUncalibrated().getV(); - if (gyro.totalSize().wordCount) { - scene.gyro_sensor = gyro[1]; - } - } - } - } if (sm.updated("wideRoadCameraState")) { - auto camera_state = sm["wideRoadCameraState"].getWideRoadCameraState(); - - float max_lines = 1618; - float max_gain = 10.0; - float max_ev = max_lines * max_gain / 6; - - float ev = camera_state.getGain() * float(camera_state.getIntegLines()); - - scene.light_sensor = std::clamp(1.0 - (ev / max_ev), 0.0, 1.0); + scene.light_sensor = 100.0f - sm["wideRoadCameraState"].getWideRoadCameraState().getExposureValPercent(); } scene.started = sm["deviceState"].getDeviceState().getStarted() && scene.ignition; } @@ -224,12 +201,19 @@ void UIState::updateStatus() { started_prev = scene.started; emit offroadTransition(!scene.started); } + + // Handle prime type change + if (prime_type != prime_type_prev) { + prime_type_prev = prime_type; + emit primeTypeChanged(prime_type); + Params().put("PrimeType", std::to_string(prime_type)); + } } UIState::UIState(QObject *parent) : QObject(parent) { sm = std::make_unique>({ "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", "roadCameraState", - "pandaStates", "carParams", "driverMonitoringState", "sensorEvents", "carState", "liveLocationKalman", + "pandaStates", "carParams", "driverMonitoringState", "carState", "liveLocationKalman", "wideRoadCameraState", "managerState", "navInstruction", "navRoute", "gnssMeasurements", }); @@ -286,8 +270,7 @@ void Device::resetInteractiveTimout() { void Device::updateBrightness(const UIState &s) { float clipped_brightness = BACKLIGHT_OFFROAD; if (s.scene.started) { - // Scale to 0% to 100% - clipped_brightness = 100.0 * s.scene.light_sensor; + clipped_brightness = s.scene.light_sensor; // CIE 1931 - https://www.photonstophotos.net/GeneralTopics/Exposure/Psychometric_Lightness_and_Gamma.htm if (clipped_brightness <= 8) { @@ -313,24 +296,11 @@ void Device::updateBrightness(const UIState &s) { } } -bool Device::motionTriggered(const UIState &s) { - static float accel_prev = 0; - static float gyro_prev = 0; - - bool accel_trigger = abs(s.scene.accel_sensor - accel_prev) > 0.2; - bool gyro_trigger = abs(s.scene.gyro_sensor - gyro_prev) > 0.15; - - gyro_prev = s.scene.gyro_sensor; - accel_prev = (accel_prev * (accel_samples - 1) + s.scene.accel_sensor) / accel_samples; - - return (!awake && accel_trigger && gyro_trigger); -} - void Device::updateWakefulness(const UIState &s) { bool ignition_just_turned_off = !s.scene.ignition && ignition_on; ignition_on = s.scene.ignition; - if (ignition_just_turned_off || motionTriggered(s)) { + if (ignition_just_turned_off) { resetInteractiveTimout(); } else if (interactive_timeout > 0 && --interactive_timeout == 0) { emit interactiveTimout(); diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index b4ec900eef..f60c26b59a 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -100,7 +100,7 @@ typedef struct UIScene { // lead QPointF lead_vertices[2]; - float light_sensor, accel_sensor, gyro_sensor; + float light_sensor; bool started, ignition, is_metric, map_on_left, longitudinal_control, end_to_end_long; uint64_t started_frame; } UIScene; @@ -126,7 +126,7 @@ public: UIScene scene = {}; bool awake; - int prime_type = 0; + int prime_type; QString language; QTransform car_space_transform; @@ -135,6 +135,7 @@ public: signals: void uiUpdate(const UIState &s); void offroadTransition(bool offroad); + void primeTypeChanged(int prime_type); private slots: void update(); @@ -142,6 +143,7 @@ private slots: private: QTimer *timer; bool started_prev = false; + int prime_type_prev = -1; }; UIState *uiState(); @@ -155,9 +157,6 @@ public: Device(QObject *parent = 0); private: - // auto brightness - const float accel_samples = 5*UI_FREQ; - bool awake = false; int interactive_timeout = 0; bool ignition_on = false; diff --git a/selfdrive/updated.py b/selfdrive/updated.py index c806e726c6..9568b28ae3 100755 --- a/selfdrive/updated.py +++ b/selfdrive/updated.py @@ -1,27 +1,4 @@ #!/usr/bin/env python3 - -# Safe Update: A simple service that waits for network access and tries to -# update every 10 minutes. It's intended to make the OP update process more -# robust against Git repository corruption. This service DOES NOT try to fix -# an already-corrupt BASEDIR Git repo, only prevent it from happening. -# -# During normal operation, both onroad and offroad, the update process makes -# no changes to the BASEDIR install of OP. All update attempts are performed -# in a disposable staging area provided by OverlayFS. It assumes the deleter -# process provides enough disk space to carry out the process. -# -# If an update succeeds, a flag is set, and the update is swapped in at the -# next reboot. If an update is interrupted or otherwise fails, the OverlayFS -# upper layer and metadata can be discarded before trying again. -# -# The swap on boot is triggered by launch_chffrplus.sh -# gated on the existence of $FINALIZED/.overlay_consistent and also the -# existence and mtime of $BASEDIR/.overlay_init. -# -# Other than build byproducts, BASEDIR should not be modified while this -# service is running. Developers modifying code directly in BASEDIR should -# disable this service. - import os import re import datetime @@ -52,6 +29,8 @@ OVERLAY_METADATA = os.path.join(STAGING_ROOT, "metadata") OVERLAY_MERGED = os.path.join(STAGING_ROOT, "merged") FINALIZED = os.path.join(STAGING_ROOT, "finalized") +OVERLAY_INIT = Path(os.path.join(BASEDIR, ".overlay_init")) + DAYS_NO_CONNECTIVITY_MAX = 14 # do not allow to engage after this many days DAYS_NO_CONNECTIVITY_PROMPT = 10 # send an offroad prompt after this many days @@ -134,12 +113,10 @@ def dismount_overlay() -> None: def init_overlay() -> None: - overlay_init_file = Path(os.path.join(BASEDIR, ".overlay_init")) - # Re-create the overlay if BASEDIR/.git has changed since we created the overlay - if overlay_init_file.is_file(): + if OVERLAY_INIT.is_file() and os.path.ismount(OVERLAY_MERGED): git_dir_path = os.path.join(BASEDIR, ".git") - new_files = run(["find", git_dir_path, "-newer", str(overlay_init_file)]) + new_files = run(["find", git_dir_path, "-newer", str(OVERLAY_INIT)]) if not len(new_files.splitlines()): # A valid overlay already exists return @@ -170,7 +147,7 @@ def init_overlay() -> None: consistent_file = Path(os.path.join(BASEDIR, ".overlay_consistent")) if consistent_file.is_file(): consistent_file.unlink() - overlay_init_file.touch() + OVERLAY_INIT.touch() os.sync() overlay_opts = f"lowerdir={BASEDIR},upperdir={OVERLAY_UPPER},workdir={OVERLAY_METADATA}" @@ -241,6 +218,11 @@ class Updater: def __init__(self): self.params = Params() self.branches = defaultdict(lambda: '') + self._has_internet: bool = False + + @property + def has_internet(self) -> bool: + return self._has_internet @property def target_branch(self) -> str: @@ -321,7 +303,7 @@ class Updater: now = datetime.datetime.utcnow() dt = now - last_update - if failed_count > 15 and exception is not None: + if failed_count > 15 and exception is not None and self.has_internet: if is_tested_branch(): extra_text = "Ensure the software is correctly installed. Uninstall and re-install if this error persists." else: @@ -338,6 +320,12 @@ class Updater: excluded_branches = ('release2', 'release2-staging', 'dashcam', 'dashcam-staging') + try: + run(["git", "ls-remote", "origin", "HEAD"], OVERLAY_MERGED) + self._has_internet = True + except subprocess.CalledProcessError: + self._has_internet = False + setup_git_options(OVERLAY_MERGED) output = run(["git", "ls-remote", "--heads"], OVERLAY_MERGED) @@ -353,9 +341,9 @@ class Updater: new_branch = self.target_branch new_commit = self.branches[new_branch] if (cur_branch, cur_commit) != (new_branch, new_commit): - cloudlog.info(f"update available, {cur_branch} ({cur_commit[:7]}) -> {new_branch} ({new_commit[:7]})") + cloudlog.info(f"update available, {cur_branch} ({str(cur_commit)[:7]}) -> {new_branch} ({str(new_commit)[:7]})") else: - cloudlog.info(f"up to date on {cur_branch} ({cur_commit[:7]})") + cloudlog.info(f"up to date on {cur_branch} ({str(cur_commit)[:7]})") def fetch_update(self) -> None: cloudlog.info("attempting git fetch inside staging overlay") @@ -376,7 +364,7 @@ class Updater: cmds = [ ["git", "checkout", "--force", "--no-recurse-submodules", "-B", branch, "FETCH_HEAD"], ["git", "reset", "--hard"], - ["git", "clean", "-xdf"], + ["git", "clean", "-xdff"], ["git", "submodule", "init"], ["git", "submodule", "update"], ] @@ -419,9 +407,6 @@ def main() -> None: t = datetime.datetime.utcnow().isoformat() params.put("InstallDate", t.encode('utf8')) - overlay_init = Path(os.path.join(BASEDIR, ".overlay_init")) - overlay_init.unlink(missing_ok=True) - updater = Updater() update_failed_count = 0 # TODO: Load from param? @@ -461,11 +446,11 @@ def main() -> None: returncode=e.returncode ) exception = f"command failed: {e.cmd}\n{e.output}" - overlay_init.unlink(missing_ok=True) + OVERLAY_INIT.unlink(missing_ok=True) except Exception as e: cloudlog.exception("uncaught updated exception, shouldn't happen") exception = str(e) - overlay_init.unlink(missing_ok=True) + OVERLAY_INIT.unlink(missing_ok=True) try: params.put("UpdaterState", "idle") diff --git a/system/camerad/cameras/camera_common.cc b/system/camerad/cameras/camera_common.cc index 580c4bc5ee..30e2810ec4 100644 --- a/system/camerad/cameras/camera_common.cc +++ b/system/camerad/cameras/camera_common.cc @@ -162,6 +162,10 @@ void fill_frame_data(cereal::FrameData::Builder &framed, const FrameMetadata &fr framed.setLensTruePos(frame_data.lens_true_pos); framed.setProcessingTime(frame_data.processing_time); + const float ev = c->cur_ev[frame_data.frame_id % 3]; + const float perc = util::map_val(ev, c->min_ev, c->max_ev, 0.0f, 100.0f); + framed.setExposureValPercent(perc); + if (c->camera_id == CAMERA_ID_AR0231) { framed.setSensor(cereal::FrameData::ImageSensor::AR0321); } else if (c->camera_id == CAMERA_ID_OX03C10) { diff --git a/system/camerad/cameras/camera_qcom2.cc b/system/camerad/cameras/camera_qcom2.cc index 65a0de0a6f..78848af5be 100644 --- a/system/camerad/cameras/camera_qcom2.cc +++ b/system/camerad/cameras/camera_qcom2.cc @@ -1078,8 +1078,8 @@ void CameraState::set_camera_exposure(float grey_frac) { std::string gain_bytes, time_bytes; if (env_ctrl_exp_from_params) { - gain_bytes = Params().get("CameraDebugExpGain"); - time_bytes = Params().get("CameraDebugExpTime"); + gain_bytes = Params().get("CameraDebugExpGain"); + time_bytes = Params().get("CameraDebugExpTime"); } if (gain_bytes.size() > 0 && time_bytes.size() > 0) { @@ -1150,10 +1150,10 @@ void CameraState::set_camera_exposure(float grey_frac) { if (camera_id == CAMERA_ID_AR0231) { uint16_t analog_gain_reg = 0xFF00 | (new_g << 4) | new_g; struct i2c_random_wr_payload exp_reg_array[] = { - {0x3366, analog_gain_reg}, - {0x3362, (uint16_t)(dc_gain_enabled ? 0x1 : 0x0)}, - {0x3012, (uint16_t)exposure_time}, - }; + {0x3366, analog_gain_reg}, + {0x3362, (uint16_t)(dc_gain_enabled ? 0x1 : 0x0)}, + {0x3012, (uint16_t)exposure_time}, + }; sensors_i2c(exp_reg_array, sizeof(exp_reg_array)/sizeof(struct i2c_random_wr_payload), CAM_SENSOR_PACKET_OPCODE_SENSOR_CONFIG, true); } else if (camera_id == CAMERA_ID_OX03C10) { // t_HCG + t_LCG + t_VS on LPD, t_SPD on SPD @@ -1166,7 +1166,6 @@ void CameraState::set_camera_exposure(float grey_frac) { uint32_t real_gain = ox03c10_analog_gains_reg[new_g]; uint32_t min_gain = ox03c10_analog_gains_reg[0]; struct i2c_random_wr_payload exp_reg_array[] = { - {0x3501, hcg_time>>8}, {0x3502, hcg_time&0xFF}, {0x3581, lcg_time>>8}, {0x3582, lcg_time&0xFF}, {0x3541, spd_time>>8}, {0x3542, spd_time&0xFF}, diff --git a/system/camerad/test/test_camerad.py b/system/camerad/test/test_camerad.py index 1a2e365a8f..378d4b7058 100755 --- a/system/camerad/test/test_camerad.py +++ b/system/camerad/test/test_camerad.py @@ -1,25 +1,18 @@ #!/usr/bin/env python3 - import time import unittest +from collections import defaultdict import cereal.messaging as messaging +from cereal.services import service_list +from selfdrive.manager.process_config import managed_processes from system.hardware import TICI -from selfdrive.test.helpers import with_processes -TEST_TIMESPAN = 30 # random.randint(60, 180) # seconds -SKIP_FRAME_TOLERANCE = 0 -LAG_FRAME_TOLERANCE = 2 # ms +TEST_TIMESPAN = 30 +LAG_FRAME_TOLERANCE = 0.5 # ms -FPS_BASELINE = 20 -CAMERAS = { - "roadCameraState": FPS_BASELINE, - "driverCameraState": FPS_BASELINE // 2, -} +CAMERAS = ('roadCameraState', 'driverCameraState', 'wideRoadCameraState') -if TICI: - CAMERAS["driverCameraState"] = FPS_BASELINE - CAMERAS["wideRoadCameraState"] = FPS_BASELINE class TestCamerad(unittest.TestCase): @classmethod @@ -27,37 +20,57 @@ class TestCamerad(unittest.TestCase): if not TICI: raise unittest.SkipTest - @with_processes(['camerad']) - def test_frame_packets(self): - print("checking frame pkts continuity") - print(TEST_TIMESPAN) + # run camerad and record logs + managed_processes['camerad'].start() + time.sleep(3) + socks = {c: messaging.sub_sock(c, conflate=False, timeout=100) for c in CAMERAS} + + cls.logs = defaultdict(list) + start_time = time.monotonic() + while time.monotonic()- start_time < TEST_TIMESPAN: + for cam, s in socks.items(): + cls.logs[cam] += messaging.drain_sock(s) + time.sleep(0.2) + managed_processes['camerad'].stop() - sm = messaging.SubMaster([socket_name for socket_name in CAMERAS]) + cls.log_by_frame_id = defaultdict(list) + for cam, msgs in cls.logs.items(): + expected_frames = service_list[cam].frequency * TEST_TIMESPAN + assert expected_frames*0.95 < len(msgs) < expected_frames*1.05, f"unexpected frame count {cam}: {expected_frames=}, got {len(msgs)}" - last_frame_id = dict.fromkeys(CAMERAS, None) - last_ts = dict.fromkeys(CAMERAS, None) - start_time_sec = time.time() - while time.time()- start_time_sec < TEST_TIMESPAN: - sm.update() + for m in msgs: + cls.log_by_frame_id[getattr(m, m.which()).frameId].append(m) - for camera in CAMERAS: - if sm.updated[camera]: - ct = (sm[camera].timestampEof if not TICI else sm[camera].timestampSof) / 1e6 - if last_frame_id[camera] is None: - last_frame_id[camera] = sm[camera].frameId - last_ts[camera] = ct - continue + # strip beginning and end + for _ in range(3): + mn, mx = min(cls.log_by_frame_id.keys()), max(cls.log_by_frame_id.keys()) + del cls.log_by_frame_id[mn] + del cls.log_by_frame_id[mx] + + @classmethod + def tearDownClass(cls): + managed_processes['camerad'].stop() - dfid = sm[camera].frameId - last_frame_id[camera] - self.assertTrue(abs(dfid - 1) <= SKIP_FRAME_TOLERANCE, "%s frame id diff is %d" % (camera, dfid)) + def test_frame_skips(self): + skips = {} + frame_ids = self.log_by_frame_id.keys() + for frame_id in range(min(frame_ids), max(frame_ids)): + seen_cams = [msg.which() for msg in self.log_by_frame_id[frame_id]] + skip_cams = set(CAMERAS) - set(seen_cams) + if len(skip_cams): + skips[frame_id] = skip_cams + assert len(skips) == 0, f"Found frame skips, missing cameras for the following frames: {skips}" - dts = ct - last_ts[camera] - self.assertTrue(abs(dts - (1000/CAMERAS[camera])) < LAG_FRAME_TOLERANCE, f"{camera} frame t(ms) diff is {dts:f}") + def test_frame_sync(self): + frame_times = {frame_id: [getattr(m, m.which()).timestampSof for m in msgs] for frame_id, msgs in self.log_by_frame_id.items()} + diffs = {frame_id: (max(ts) - min(ts))/1e6 for frame_id, ts in frame_times.items()} - last_frame_id[camera] = sm[camera].frameId - last_ts[camera] = ct - time.sleep(0.01) + def get_desc(fid, diff): + cam_times = [(m.which(), getattr(m, m.which()).timestampSof/1e6) for m in self.log_by_frame_id[fid]] + return f"{diff=} {cam_times=}" + laggy_frames = {k: get_desc(k, v) for k, v in diffs.items() if v > LAG_FRAME_TOLERANCE} + assert len(laggy_frames) == 0, f"Frames not synced properly: {laggy_frames=}" if __name__ == "__main__": unittest.main() diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index dc0fa6f1c1..8534c8a978 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -41,9 +41,9 @@ }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-9db38e27c912005472f3ac02be336af4f82307295118b6db22921479d44a941d.img.xz", - "hash": "05e7ce440b33721b020a249043d9568a5898080e26411ca250fb330ad2e5ed8e", - "hash_raw": "9db38e27c912005472f3ac02be336af4f82307295118b6db22921479d44a941d", + "url": "https://commadist.azureedge.net/agnosupdate/system-b40b08912576bb972907acba7c201c1399395cbc0cba06ce6e5e3f70ab565cb5.img.xz", + "hash": "6e8fbcc21a265f7f58062abce7675dc05540e2b60cee2df56992a151ba64936f", + "hash_raw": "b40b08912576bb972907acba7c201c1399395cbc0cba06ce6e5e3f70ab565cb5", "size": 10737418240, "sparse": true, "full_check": false, diff --git a/system/hardware/tici/agnos.py b/system/hardware/tici/agnos.py index 51998bf8b7..5f446a8e90 100755 --- a/system/hardware/tici/agnos.py +++ b/system/hardware/tici/agnos.py @@ -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}) # type: ignore # pylint: disable=missing-timeout + self.req = requests.get(url, stream=True, headers={'Accept-Encoding': None}, timeout=60) # type: ignore self.it = self.req.iter_content(chunk_size=1024 * 1024) self.decompressor = lzma.LZMADecompressor(format=lzma.FORMAT_AUTO) self.eof = False diff --git a/system/hardware/tici/casync.py b/system/hardware/tici/casync.py index 8ae42fa714..993336616d 100755 --- a/system/hardware/tici/casync.py +++ b/system/hardware/tici/casync.py @@ -86,6 +86,7 @@ class RemoteChunkReader(ChunkReader): 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 if os.path.isfile(caibx_path): caibx = open(caibx_path, 'rb') else: diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 340093b604..e2fd20c1be 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -480,7 +480,7 @@ class Tici(HardwareBase): # blue prime config if sim_id.startswith('8901410'): - os.system('mmcli -m 0 --3gpp-set-initial-eps-bearer-settings="apn=Broadband"') + os.system('mmcli -m any --3gpp-set-initial-eps-bearer-settings="apn=Broadband"') def get_networks(self): r = {} diff --git a/system/hardware/tici/test_power_draw.py b/system/hardware/tici/test_power_draw.py index 4b380372b9..4830975917 100755 --- a/system/hardware/tici/test_power_draw.py +++ b/system/hardware/tici/test_power_draw.py @@ -20,7 +20,7 @@ class Proc: PROCS = [ Proc('camerad', 2.15), - Proc('modeld', 1.0, atol=0.15), + Proc('modeld', 1.15, atol=0.2), Proc('dmonitoringmodeld', 0.35), Proc('encoderd', 0.23), ] diff --git a/system/proclogd/proclog.cc b/system/proclogd/proclog.cc index cbe3b53493..09ab4f559e 100644 --- a/system/proclogd/proclog.cc +++ b/system/proclogd/proclog.cc @@ -95,7 +95,7 @@ std::optional procStat(std::string stat) { .num_threads = stol(v[StatPos::num_threads - 1]), .starttime = stoull(v[StatPos::starttime - 1]), .vms = stoul(v[StatPos::vsize - 1]), - .rss = stoul(v[StatPos::rss - 1]), + .rss = stol(v[StatPos::rss - 1]), .processor = stoi(v[StatPos::processor - 1]), }; return p; diff --git a/system/proclogd/proclog.h b/system/proclogd/proclog.h index 9ed53d1bac..49f97cdd36 100644 --- a/system/proclogd/proclog.h +++ b/system/proclogd/proclog.h @@ -20,8 +20,8 @@ struct ProcCache { struct ProcStat { int pid, ppid, processor; char state; - long cutime, cstime, priority, nice, num_threads; - unsigned long utime, stime, vms, rss; + long cutime, cstime, priority, nice, num_threads, rss; + unsigned long utime, stime, vms; unsigned long long starttime; std::string name; }; diff --git a/system/proclogd/tests/test_proclog.cc b/system/proclogd/tests/test_proclog.cc index 230e855acb..affde2f320 100644 --- a/system/proclogd/tests/test_proclog.cc +++ b/system/proclogd/tests/test_proclog.cc @@ -140,6 +140,18 @@ TEST_CASE("buildProcLogerMessage") { REQUIRE(p.getName() == "test_proclog"); REQUIRE(p.getState() == 'R'); REQUIRE_THAT(p.getExe().cStr(), Catch::Matchers::Contains("test_proclog")); + REQUIRE(p.getCmdline().size() == 1); + REQUIRE_THAT(p.getCmdline()[0], Catch::Matchers::Contains("test_proclog")); + } else { + std::string cmd_path = "/proc/" + std::to_string(p.getPid()) + "/cmdline"; + if (util::file_exists(cmd_path)) { + std::ifstream stream(cmd_path); + auto cmdline = Parser::cmdline(stream); + REQUIRE(cmdline.size() == p.getCmdline().size()); + for (int i = 0; i < p.getCmdline().size(); ++i) { + REQUIRE(cmdline[i] == p.getCmdline()[i].cStr()); + } + } } } } diff --git a/tinygrad_repo b/tinygrad_repo index 2e9b7637b3..870ea766ee 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 2e9b7637b3c3c8895fda9f964215db3a35fe3441 +Subproject commit 870ea766eec7a38d7d590c81436f15271ba2667e diff --git a/tools/cabana/.gitignore b/tools/cabana/.gitignore new file mode 100644 index 0000000000..88ffab2717 --- /dev/null +++ b/tools/cabana/.gitignore @@ -0,0 +1,5 @@ +moc_* +*.moc + +_cabana +settings diff --git a/tools/cabana/README.md b/tools/cabana/README.md new file mode 100644 index 0000000000..f64e6b2d2d --- /dev/null +++ b/tools/cabana/README.md @@ -0,0 +1,9 @@ +# Cabana + + + +Cabana is a tool developed to view raw CAN data. One use for this is creating and editing [CAN Dictionaries](http://socialledge.com/sjsu/index.php/DBC_Format) (DBC files), and the tool provides direct integration with [commaai/opendbc](https://github.com/commaai/opendbc) (a collection of DBC files), allowing you to load the DBC files direct from source, and save to your fork. In addition, you can load routes from [comma connect](https://connect.comma.ai). + +## Usage Instructions + +See [openpilot wiki](https://github.com/commaai/openpilot/wiki/Cabana) diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript new file mode 100644 index 0000000000..8dbd4f1d1c --- /dev/null +++ b/tools/cabana/SConscript @@ -0,0 +1,16 @@ +Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib', + 'cereal', 'transformations', 'widgets', 'opendbc') + +base_frameworks = qt_env['FRAMEWORKS'] +base_libs = [common, messaging, cereal, visionipc, transformations, 'zmq', + 'capnp', 'kj', 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] + +if arch == "Darwin": + base_frameworks.append('OpenCL') +else: + base_libs.append('OpenCL') + +qt_libs = ['qt_util', 'Qt5Charts'] + base_libs +cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, opendbc,'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv'] + qt_libs +qt_env.Program('_cabana', ['cabana.cc', 'mainwin.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', 'dbcmanager.cc', + 'canmessages.cc', 'messageswidget.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) diff --git a/tools/cabana/cabana b/tools/cabana/cabana new file mode 100755 index 0000000000..b29dd66e3d --- /dev/null +++ b/tools/cabana/cabana @@ -0,0 +1,4 @@ +#!/bin/sh +cd "$(dirname "$0")" +export LD_LIBRARY_PATH="../../opendbc/can:$LD_LIBRARY_PATH" +exec ./_cabana "$1" diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc new file mode 100644 index 0000000000..1096487973 --- /dev/null +++ b/tools/cabana/cabana.cc @@ -0,0 +1,33 @@ +#include +#include +#include + +#include "selfdrive/ui/qt/util.h" +#include "tools/cabana/mainwin.h" + +int main(int argc, char *argv[]) { + initApp(argc, argv); + QApplication app(argc, argv); + app.setStyle(QStyleFactory::create("Fusion")); + + QCommandLineParser cmd_parser; + cmd_parser.addHelpOption(); + cmd_parser.addPositionalArgument("route", "the drive to replay. find your drives at connect.comma.ai"); + cmd_parser.addOption({"demo", "use a demo route instead of providing your own"}); + cmd_parser.addOption({"qcam", "load qcamera"}); + cmd_parser.addOption({"data_dir", "local directory with routes", "data_dir"}); + cmd_parser.process(app); + const QStringList args = cmd_parser.positionalArguments(); + if (args.empty() && !cmd_parser.isSet("demo")) { + cmd_parser.showHelp(); + } + + const QString route = args.empty() ? DEMO_ROUTE : args.first(); + CANMessages p(&app); + if (!p.loadRoute(route, cmd_parser.value("data_dir"), cmd_parser.isSet("qcam"))) { + return 0; + } + MainWindow w; + w.showMaximized(); + return app.exec(); +} diff --git a/tools/cabana/canmessages.cc b/tools/cabana/canmessages.cc new file mode 100644 index 0000000000..b717424ece --- /dev/null +++ b/tools/cabana/canmessages.cc @@ -0,0 +1,151 @@ +#include "tools/cabana/canmessages.h" + +#include +#include + +Q_DECLARE_METATYPE(std::vector); + +Settings settings; +CANMessages *can = nullptr; + +CANMessages::CANMessages(QObject *parent) : QObject(parent) { + can = this; + + qRegisterMetaType>(); + QObject::connect(this, &CANMessages::received, this, &CANMessages::process, Qt::QueuedConnection); + QObject::connect(&settings, &Settings::changed, this, &CANMessages::settingChanged); +} + +CANMessages::~CANMessages() { + replay->stop(); +} + +static bool event_filter(const Event *e, void *opaque) { + CANMessages *c = (CANMessages *)opaque; + return c->eventFilter(e); +} + +bool CANMessages::loadRoute(const QString &route, const QString &data_dir, bool use_qcam) { + replay = new Replay(route, {"can", "roadEncodeIdx"}, {}, nullptr, use_qcam ? REPLAY_FLAG_QCAMERA : 0, data_dir, this); + replay->setSegmentCacheLimit(settings.cached_segment_limit); + replay->installEventFilter(event_filter, this); + QObject::connect(replay, &Replay::segmentsMerged, this, &CANMessages::segmentsMerged); + if (replay->load()) { + replay->start(); + return true; + } + return false; +} + +void CANMessages::process(QHash> *messages) { + for (auto it = messages->begin(); it != messages->end(); ++it) { + ++counters[it.key()]; + auto &msgs = can_msgs[it.key()]; + const auto &new_msgs = it.value(); + if (new_msgs.size() == settings.can_msg_log_size || msgs.empty()) { + msgs = std::move(new_msgs); + } else { + msgs.insert(msgs.begin(), std::make_move_iterator(new_msgs.begin()), std::make_move_iterator(new_msgs.end())); + while (msgs.size() >= settings.can_msg_log_size) { + msgs.pop_back(); + } + } + } + delete messages; + + if (current_sec < begin_sec || current_sec > end_sec) { + // loop replay in selected range. + seekTo(begin_sec); + } else { + emit updated(); + } +} + +bool CANMessages::eventFilter(const Event *event) { + static double prev_update_sec = 0; + // drop packets when the GUI thread is calling seekTo. to make sure the current_sec is accurate. + if (!seeking && event->which == cereal::Event::Which::CAN) { + if (!received_msgs) { + received_msgs.reset(new QHash>); + received_msgs->reserve(1000); + } + + current_sec = (event->mono_time - replay->routeStartTime()) / (double)1e9; + auto can_events = event->event.getCan(); + for (const auto &c : can_events) { + QString id = QString("%1:%2").arg(c.getSrc()).arg(c.getAddress(), 1, 16); + auto &list = (*received_msgs)[id]; + while (list.size() >= settings.can_msg_log_size) { + list.pop_back(); + } + CanData &data = list.emplace_front(); + data.ts = current_sec; + data.bus_time = c.getBusTime(); + data.dat.append((char *)c.getDat().begin(), c.getDat().size()); + } + + if (current_sec < prev_update_sec || (current_sec - prev_update_sec) > 1.0 / settings.fps) { + prev_update_sec = current_sec; + // use pointer to avoid data copy in queued connection. + emit received(received_msgs.release()); + } + } + return true; +} + +void CANMessages::seekTo(double ts) { + seeking = true; + replay->seekTo(ts, false); + seeking = false; +} + +void CANMessages::setRange(double min, double max) { + if (begin_sec != min || end_sec != max) { + begin_sec = min; + end_sec = max; + is_zoomed = begin_sec != event_begin_sec || end_sec != event_end_sec; + emit rangeChanged(min, max); + } +} + +void CANMessages::segmentsMerged() { + auto events = replay->events(); + if (!events || events->empty()) return; + + auto it = std::find_if(events->begin(), events->end(), [=](const Event *e) { return e->which == cereal::Event::Which::CAN; }); + event_begin_sec = it == events->end() ? 0 : ((*it)->mono_time - replay->routeStartTime()) / (double)1e9; + event_end_sec = double(events->back()->mono_time - replay->routeStartTime()) / 1e9; + if (!is_zoomed) { + begin_sec = event_begin_sec; + end_sec = event_end_sec; + } + emit eventsMerged(); +} + +void CANMessages::resetRange() { + setRange(event_begin_sec, event_end_sec); +} + +void CANMessages::settingChanged() { + replay->setSegmentCacheLimit(settings.cached_segment_limit); +} + +// Settings + +Settings::Settings() { + load(); +} + +void Settings::save() { + QSettings s("settings", QSettings::IniFormat); + s.setValue("fps", fps); + s.setValue("log_size", can_msg_log_size); + s.setValue("cached_segment", cached_segment_limit); +} + +void Settings::load() { + QSettings s("settings", QSettings::IniFormat); + fps = s.value("fps", 10).toInt(); + can_msg_log_size = s.value("log_size", 100).toInt(); + cached_segment_limit = s.value("cached_segment", 3.).toInt(); +} diff --git a/tools/cabana/canmessages.h b/tools/cabana/canmessages.h new file mode 100644 index 0000000000..3d33f801a7 --- /dev/null +++ b/tools/cabana/canmessages.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include + +#include + +#include "tools/replay/replay.h" + +class Settings : public QObject { + Q_OBJECT + +public: + Settings(); + void save(); + void load(); + + int fps = 10; + int can_msg_log_size = 100; + int cached_segment_limit = 3; + +signals: + void changed(); +}; + +struct CanData { + double ts; + uint16_t bus_time; + QByteArray dat; +}; + +class CANMessages : public QObject { + Q_OBJECT + +public: + CANMessages(QObject *parent); + ~CANMessages(); + bool loadRoute(const QString &route, const QString &data_dir, bool use_qcam); + void seekTo(double ts); + void resetRange(); + void setRange(double min, double max); + bool eventFilter(const Event *event); + + inline std::pair range() const { return {begin_sec, end_sec}; } + inline double totalSeconds() const { return replay->totalSeconds(); } + inline double routeStartTime() const { return replay->routeStartTime() / (double)1e9; } + inline double currentSec() const { return current_sec; } + inline bool isZoomed() const { return is_zoomed; } + inline const std::deque &messages(const QString &id) { return can_msgs[id]; } + inline const CanData &lastMessage(const QString &id) { return can_msgs[id].front(); } + + inline const std::vector *events() const { return replay->events(); } + inline void setSpeed(float speed) { replay->setSpeed(speed); } + inline bool isPaused() const { return replay->isPaused(); } + inline void pause(bool pause) { replay->pause(pause); } + inline const std::vector> getTimeline() { return replay->getTimeline(); } + +signals: + void eventsMerged(); + void rangeChanged(double min, double max); + void updated(); + void received(QHash> *); + +public: + QMap> can_msgs; + std::unique_ptr>> received_msgs = nullptr; + QHash counters; + +protected: + void process(QHash> *); + void segmentsMerged(); + void settingChanged(); + + std::atomic current_sec = 0.; + std::atomic seeking = false; + double begin_sec = 0; + double end_sec = 0; + double event_begin_sec = 0; + double event_end_sec = 0; + bool is_zoomed = false; + Replay *replay = nullptr; +}; + +inline QString toHex(const QByteArray &dat) { + return dat.toHex(' ').toUpper(); +} +inline char toHex(uint value) { + return "0123456789ABCDEF"[value & 0xF]; +} + +inline const QString &getColor(int i) { + static const QString SIGNAL_COLORS[] = {"#9FE2BF", "#40E0D0", "#6495ED", "#CCCCFF", "#FF7F50", "#FFBF00"}; + return SIGNAL_COLORS[i % std::size(SIGNAL_COLORS)]; +} + +// A global pointer referring to the unique CANMessages object +extern CANMessages *can; +extern Settings settings; diff --git a/tools/cabana/chartswidget.cc b/tools/cabana/chartswidget.cc new file mode 100644 index 0000000000..d0f5356fac --- /dev/null +++ b/tools/cabana/chartswidget.cc @@ -0,0 +1,337 @@ +#include "tools/cabana/chartswidget.h" + +#include +#include +#include +#include + +// ChartsWidget + +ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + // title bar + title_bar = new QWidget(this); + title_bar->setVisible(false); + QHBoxLayout *title_layout = new QHBoxLayout(title_bar); + title_layout->setContentsMargins(0, 0, 0, 0); + title_label = new QLabel(tr("Charts")); + + title_layout->addWidget(title_label); + title_layout->addStretch(); + + range_label = new QLabel(); + title_layout->addWidget(range_label); + + reset_zoom_btn = new QPushButton("⟲", this); + reset_zoom_btn->setFixedSize(30, 30); + reset_zoom_btn->setToolTip(tr("Reset zoom (drag on chart to zoom X-Axis)")); + title_layout->addWidget(reset_zoom_btn); + + remove_all_btn = new QPushButton("✖", this); + remove_all_btn->setToolTip(tr("Remove all charts")); + remove_all_btn->setFixedSize(30, 30); + title_layout->addWidget(remove_all_btn); + + dock_btn = new QPushButton(); + dock_btn->setFixedSize(30, 30); + title_layout->addWidget(dock_btn); + + main_layout->addWidget(title_bar, 0, Qt::AlignTop); + + // charts + QWidget *charts_container = new QWidget(this); + QVBoxLayout *charts_main = new QVBoxLayout(charts_container); + charts_layout = new QVBoxLayout(); + charts_main->addLayout(charts_layout); + charts_main->addStretch(); + + QScrollArea *charts_scroll = new QScrollArea(this); + charts_scroll->setWidgetResizable(true); + charts_scroll->setWidget(charts_container); + charts_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + main_layout->addWidget(charts_scroll); + + QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll); + QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartsWidget::removeChart); + QObject::connect(dbc(), &DBCManager::signalUpdated, [this](const Signal *sig) { + if (auto it = charts.find(sig); it != charts.end()) { + it.value()->chart_view->updateSeries(); + } + }); + QObject::connect(dbc(), &DBCManager::msgUpdated, [this](const QString &id) { + for (auto chart : charts) { + if (chart->id == id) + chart->updateTitle(); + } + }); + + QObject::connect(can, &CANMessages::rangeChanged, [this]() { updateTitleBar(); }); + QObject::connect(reset_zoom_btn, &QPushButton::clicked, can, &CANMessages::resetRange); + QObject::connect(remove_all_btn, &QPushButton::clicked, this, &ChartsWidget::removeAll); + QObject::connect(dock_btn, &QPushButton::clicked, [this]() { + emit dock(!docking); + docking = !docking; + updateTitleBar(); + }); +} + +void ChartsWidget::updateTitleBar() { + title_bar->setVisible(!charts.isEmpty()); + if (charts.isEmpty()) return; + + // show select range + range_label->setVisible(can->isZoomed()); + reset_zoom_btn->setEnabled(can->isZoomed()); + if (can->isZoomed()) { + auto [min, max] = can->range(); + range_label->setText(tr("%1 - %2").arg(min, 0, 'f', 2).arg(max, 0, 'f', 2)); + } + + title_label->setText(tr("Charts (%1)").arg(charts.size())); + dock_btn->setText(docking ? "⬈" : "⬋"); + dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts")); +} + +void ChartsWidget::addChart(const QString &id, const Signal *sig) { + if (!charts.contains(sig)) { + auto chart = new ChartWidget(id, sig, this); + QObject::connect(chart, &ChartWidget::remove, [=]() { removeChart(sig); }); + charts_layout->insertWidget(0, chart); + charts.insert(sig, chart); + } + updateTitleBar(); +} + +void ChartsWidget::removeChart(const Signal *sig) { + auto it = charts.find(sig); + if (it != charts.end()) { + it.value()->deleteLater(); + charts.remove(sig); + } + updateTitleBar(); +} + +void ChartsWidget::removeAll() { + for (auto chart : charts) + chart->deleteLater(); + charts.clear(); + updateTitleBar(); +} + +bool ChartsWidget::eventFilter(QObject *obj, QEvent *event) { + if (obj != this && event->type() == QEvent::Close) { + emit dock_btn->clicked(); + return true; + } + return false; +} + +// ChartWidget + +ChartWidget::ChartWidget(const QString &id, const Signal *sig, QWidget *parent) : id(id), signal(sig), QWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setSpacing(0); + main_layout->setContentsMargins(0, 0, 0, 0); + + QWidget *header = new QWidget(this); + header->setStyleSheet("background-color:white"); + QHBoxLayout *header_layout = new QHBoxLayout(header); + header_layout->setContentsMargins(11, 11, 11, 0); + title = new QLabel(tr("%1 %2").arg(dbc()->msg(id)->name.c_str()).arg(id)); + header_layout->addWidget(title); + header_layout->addStretch(); + + QPushButton *remove_btn = new QPushButton("✖", this); + remove_btn->setFixedSize(30, 30); + remove_btn->setToolTip(tr("Remove chart")); + QObject::connect(remove_btn, &QPushButton::clicked, this, &ChartWidget::remove); + header_layout->addWidget(remove_btn); + main_layout->addWidget(header); + + chart_view = new ChartView(id, sig, this); + chart_view->setFixedHeight(300); + main_layout->addWidget(chart_view); + main_layout->addStretch(); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); +} + +void ChartWidget::updateTitle() { + title->setText(tr("%1 %2").arg(dbc()->msg(id)->name.c_str()).arg(id)); +} + +// ChartView + +ChartView::ChartView(const QString &id, const Signal *sig, QWidget *parent) + : id(id), signal(sig), QChartView(nullptr, parent) { + QLineSeries *series = new QLineSeries(); + series->setUseOpenGL(true); + QChart *chart = new QChart(); + chart->setTitle(sig->name.c_str()); + chart->addSeries(series); + chart->createDefaultAxes(); + chart->legend()->hide(); + QFont font; + font.setBold(true); + chart->setTitleFont(font); + chart->setMargins({0, 0, 0, 0}); + chart->layout()->setContentsMargins(0, 0, 0, 0); + + track_line = new QGraphicsLineItem(chart); + track_line->setPen(QPen(Qt::gray, 1, Qt::DashLine)); + value_text = new QGraphicsSimpleTextItem(chart); + value_text->setBrush(Qt::gray); + line_marker = new QGraphicsLineItem(chart); + line_marker->setPen(QPen(Qt::black, 2)); + + setChart(chart); + + setRenderHint(QPainter::Antialiasing); + setRubberBand(QChartView::HorizontalRubberBand); + if (auto rubber = findChild()) { + QPalette pal; + pal.setBrush(QPalette::Base, QColor(0, 0, 0, 80)); + rubber->setPalette(pal); + } + + QObject::connect(can, &CANMessages::updated, this, &ChartView::updateState); + QObject::connect(can, &CANMessages::rangeChanged, this, &ChartView::rangeChanged); + QObject::connect(can, &CANMessages::eventsMerged, this, &ChartView::updateSeries); + QObject::connect(dynamic_cast(chart->axisX()), &QValueAxis::rangeChanged, can, &CANMessages::setRange); + + updateSeries(); +} + +void ChartView::updateState() { + auto axis_x = dynamic_cast(chart()->axisX()); + int x = chart()->plotArea().left() + chart()->plotArea().width() * (can->currentSec() - axis_x->min()) / (axis_x->max() - axis_x->min()); + line_marker->setLine(x, 0, x, height()); +} + +void ChartView::updateSeries() { + chart()->setTitle(signal->name.c_str()); + auto events = can->events(); + if (!events) return; + + auto l = id.split(':'); + int bus = l[0].toInt(); + uint32_t address = l[1].toUInt(nullptr, 16); + + vals.clear(); + vals.reserve(3 * 60 * 100); + uint64_t route_start_time = can->routeStartTime(); + for (auto &evt : *events) { + if (evt->which == cereal::Event::Which::CAN) { + for (auto c : evt->event.getCan()) { + if (bus == c.getSrc() && address == c.getAddress()) { + auto dat = c.getDat(); + double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *signal); + double ts = (evt->mono_time / (double)1e9) - route_start_time; // seconds + vals.push_back({ts, value}); + } + } + } + } + QLineSeries *series = (QLineSeries *)chart()->series()[0]; + series->replace(vals); + series->setPointLabelsColor(Qt::black); + auto [begin, end] = can->range(); + chart()->axisX()->setRange(begin, end); + updateAxisY(); +} + +void ChartView::rangeChanged(qreal min, qreal max) { + auto axis_x = dynamic_cast(chart()->axisX()); + if (axis_x->min() != min || axis_x->max() != max) { + axis_x->setRange(min, max); + } + updateAxisY(); +} + +// auto zoom on yaxis +void ChartView::updateAxisY() { + const auto axis_x = dynamic_cast(chart()->axisX()); + const auto axis_y = dynamic_cast(chart()->axisY()); + // vals is a sorted list + auto begin = std::lower_bound(vals.begin(), vals.end(), axis_x->min(), [](auto &p, double x) { return p.x() < x; }); + if (begin == vals.end()) + return; + + auto end = std::upper_bound(vals.begin(), vals.end(), axis_x->max(), [](double x, auto &p) { return x < p.x(); }); + const auto [min, max] = std::minmax_element(begin, end, [](auto &p1, auto &p2) { return p1.y() < p2.y(); }); + if (min->y() == max->y()) { + if (max->y() < 0) { + axis_y->setRange(max->y(), 0); + } else { + axis_y->setRange(0, max->y() == 0 ? 1 : max->y()); + } + } else { + axis_y->setRange(min->y(), max->y()); + } +} + +void ChartView::enterEvent(QEvent *event) { + track_line->setVisible(true); + value_text->setVisible(true); + QChartView::enterEvent(event); +} + +void ChartView::leaveEvent(QEvent *event) { + track_line->setVisible(false); + value_text->setVisible(false); + QChartView::leaveEvent(event); +} + +void ChartView::mouseReleaseEvent(QMouseEvent *event) { + auto rubber = findChild(); + if (event->button() == Qt::LeftButton && rubber && rubber->isVisible()) { + auto [begin, end] = can->range(); + if (rubber->width() <= 0) { + double seek_to = begin + ((event->pos().x() - chart()->plotArea().x()) / chart()->plotArea().width()) * (end - begin); + can->seekTo(seek_to); + } else if (((double)rubber->width() / chart()->plotArea().width()) * (end - begin) < 0.5) { + // don't zoom if selected range is less than 0.5s + rubber->hide(); + event->accept(); + return; + } + } else if (event->button() == Qt::RightButton) { + // reset zoom + if (can->isZoomed()) { + can->resetRange(); + event->accept(); + return; + } + } + QChartView::mouseReleaseEvent(event); + line_marker->setVisible(true); +} + +void ChartView::mouseMoveEvent(QMouseEvent *ev) { + auto rubber = findChild(); + bool show = !(rubber && rubber->isVisible()); + + if (show) { + const auto plot_area = chart()->plotArea(); + float x = std::clamp((float)ev->pos().x(), (float)plot_area.left(), (float)plot_area.right()); + track_line->setLine(x, plot_area.top(), x, plot_area.bottom()); + + auto [begin, end] = can->range(); + double sec = begin + ((x - plot_area.x()) / plot_area.width()) * (end - begin); + auto value = std::lower_bound(vals.begin(), vals.end(), sec, [](auto &p, double x) { return p.x() < x; }); + value_text->setPos(x + 6, plot_area.bottom() - 25); + if (value != vals.end()) { + value_text->setText(QString("(%1, %2)").arg(value->x(), 0, 'f', 3).arg(value->y())); + } else { + value_text->setText("(--, --)"); + } + } + + value_text->setVisible(show); + track_line->setVisible(show); + line_marker->setVisible(show); + QChartView::mouseMoveEvent(ev); +} diff --git a/tools/cabana/chartswidget.h b/tools/cabana/chartswidget.h new file mode 100644 index 0000000000..a3c470e960 --- /dev/null +++ b/tools/cabana/chartswidget.h @@ -0,0 +1,86 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/canmessages.h" +#include "tools/cabana/dbcmanager.h" + +using namespace QtCharts; + +class ChartView : public QChartView { + Q_OBJECT + +public: + ChartView(const QString &id, const Signal *sig, QWidget *parent = nullptr); + void updateSeries(); + +private: + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *ev) override; + void enterEvent(QEvent *event) override; + void leaveEvent(QEvent *event) override; + + void rangeChanged(qreal min, qreal max); + void updateAxisY(); + void updateState(); + + QGraphicsLineItem *track_line; + QGraphicsSimpleTextItem *value_text; + QGraphicsLineItem *line_marker; + QList vals; + QString id; + const Signal *signal; +}; + +class ChartWidget : public QWidget { +Q_OBJECT + +public: + ChartWidget(const QString &id, const Signal *sig, QWidget *parent); + void updateTitle(); + +signals: + void remove(); + +public: + QString id; + const Signal *signal; + QLabel *title; + ChartView *chart_view = nullptr; +}; + +class ChartsWidget : public QWidget { + Q_OBJECT + +public: + ChartsWidget(QWidget *parent = nullptr); + void addChart(const QString &id, const Signal *sig); + void removeChart(const Signal *sig); + +signals: + void dock(bool floating); + +private: + void updateState(); + void updateTitleBar(); + void removeAll(); + bool eventFilter(QObject *obj, QEvent *event); + + QWidget *title_bar; + QLabel *title_label; + QLabel *range_label; + bool docking = true; + QPushButton *dock_btn; + QPushButton *reset_zoom_btn; + QPushButton *remove_all_btn; + QVBoxLayout *charts_layout; + QHash charts; +}; diff --git a/tools/cabana/dbcmanager.cc b/tools/cabana/dbcmanager.cc new file mode 100644 index 0000000000..5b1bddcabe --- /dev/null +++ b/tools/cabana/dbcmanager.cc @@ -0,0 +1,111 @@ +#include "tools/cabana/dbcmanager.h" + +#include + +DBCManager::DBCManager(QObject *parent) : QObject(parent) {} + +DBCManager::~DBCManager() {} + +void DBCManager::open(const QString &dbc_file_name) { + dbc_name = dbc_file_name; + dbc = const_cast(dbc_lookup(dbc_name.toStdString())); + msg_map.clear(); + for (auto &msg : dbc->msgs) { + msg_map[msg.address] = &msg; + } + emit DBCFileChanged(); +} + +void save(const QString &dbc_file_name) { + // TODO: save DBC to file +} + +void DBCManager::updateMsg(const QString &id, const QString &name, uint32_t size) { + auto m = const_cast(msg(id)); + if (m) { + m->name = name.toStdString(); + m->size = size; + } else { + uint32_t address = addressFromId(id); + dbc->msgs.push_back({.address = address, .name = name.toStdString(), .size = size}); + msg_map[address] = &dbc->msgs.back(); + } + emit msgUpdated(id); +} + +void DBCManager::addSignal(const QString &id, const Signal &sig) { + if (Msg *m = const_cast(msg(id))) { + m->sigs.push_back(sig); + emit signalAdded(&m->sigs.back()); + } +} + +void DBCManager::updateSignal(const QString &id, const QString &sig_name, const Signal &sig) { + if (Msg *m = const_cast(msg(id))) { + auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); }); + if (it != m->sigs.end()) { + *it = sig; + emit signalUpdated(&(*it)); + } + } +} + +void DBCManager::removeSignal(const QString &id, const QString &sig_name) { + if (Msg *m = const_cast(msg(id))) { + auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); }); + if (it != m->sigs.end()) { + emit signalRemoved(&(*it)); + m->sigs.erase(it); + } + } +} + +uint32_t DBCManager::addressFromId(const QString &id) { + return id.mid(id.indexOf(':') + 1).toUInt(nullptr, 16); +} + +DBCManager *dbc() { + static DBCManager dbc_manager(nullptr); + return &dbc_manager; +} + +// helper functions + +static QVector BIG_ENDIAN_START_BITS = []() { + QVector ret; + for (int i = 0; i < 64; i++) + for (int j = 7; j >= 0; j--) + ret.push_back(j + i * 8); + return ret; +}(); + +int bigEndianStartBitsIndex(int start_bit) { + return BIG_ENDIAN_START_BITS[start_bit]; +} + +int bigEndianBitIndex(int index) { + return BIG_ENDIAN_START_BITS.indexOf(index); +} + +double get_raw_value(uint8_t *data, size_t data_size, const Signal &sig) { + int64_t val = 0; + + int i = sig.msb / 8; + int bits = sig.size; + while (i >= 0 && i < data_size && bits > 0) { + int lsb = (int)(sig.lsb / 8) == i ? sig.lsb : i * 8; + int msb = (int)(sig.msb / 8) == i ? sig.msb : (i + 1) * 8 - 1; + int size = msb - lsb + 1; + + uint64_t d = (data[i] >> (lsb - (i * 8))) & ((1ULL << size) - 1); + val |= d << (bits - size); + + bits -= size; + i = sig.is_little_endian ? i - 1 : i + 1; + } + if (sig.is_signed) { + val -= ((val >> (sig.size - 1)) & 0x1) ? (1ULL << sig.size) : 0; + } + double value = val * sig.factor + sig.offset; + return value; +} diff --git a/tools/cabana/dbcmanager.h b/tools/cabana/dbcmanager.h new file mode 100644 index 0000000000..1f890a39db --- /dev/null +++ b/tools/cabana/dbcmanager.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include "opendbc/can/common_dbc.h" + +class DBCManager : public QObject { + Q_OBJECT + +public: + DBCManager(QObject *parent); + ~DBCManager(); + + void open(const QString &dbc_file_name); + void save(const QString &dbc_file_name); + + void addSignal(const QString &id, const Signal &sig); + void updateSignal(const QString &id, const QString &sig_name, const Signal &sig); + void removeSignal(const QString &id, const QString &sig_name); + + static uint32_t addressFromId(const QString &id); + inline static std::vector allDBCNames() { return get_dbc_names(); } + inline QString name() const { return dbc_name; } + + void updateMsg(const QString &id, const QString &name, uint32_t size); + inline const Msg *msg(const QString &id) const { return msg(addressFromId(id)); } + inline const Msg *msg(uint32_t address) const { + auto it = msg_map.find(address); + return it != msg_map.end() ? it->second : nullptr; + } + +signals: + void signalAdded(const Signal *sig); + void signalRemoved(const Signal *sig); + void signalUpdated(const Signal *sig); + void msgUpdated(const QString &id); + void DBCFileChanged(); + +private: + QString dbc_name; + DBC *dbc = nullptr; + std::unordered_map msg_map; +}; + +// TODO: Add helper function in dbc.h +double get_raw_value(uint8_t *data, size_t data_size, const Signal &sig); +int bigEndianStartBitsIndex(int start_bit); +int bigEndianBitIndex(int index); + +DBCManager *dbc(); diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc new file mode 100644 index 0000000000..a9899ec650 --- /dev/null +++ b/tools/cabana/detailwidget.cc @@ -0,0 +1,278 @@ +#include "tools/cabana/detailwidget.h" + +#include +#include +#include +#include +#include +#include +#include + +// DetailWidget + +DetailWidget::DetailWidget(QWidget *parent) : QWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + + // title + QHBoxLayout *title_layout = new QHBoxLayout(); + title_layout->addWidget(new QLabel("time:")); + time_label = new QLabel(this); + title_layout->addWidget(time_label); + time_label->setStyleSheet("font-weight:bold"); + title_layout->addStretch(); + name_label = new QLabel(this); + name_label->setStyleSheet("font-weight:bold;"); + title_layout->addWidget(name_label); + title_layout->addStretch(); + edit_btn = new QPushButton(tr("Edit"), this); + edit_btn->setVisible(false); + title_layout->addWidget(edit_btn); + main_layout->addLayout(title_layout); + + // binary view + binary_view = new BinaryView(this); + main_layout->addWidget(binary_view, 0, Qt::AlignTop); + + // signals + signals_container = new QWidget(this); + signals_container->setLayout(new QVBoxLayout); + signals_container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + + scroll = new ScrollArea(this); + scroll->setWidget(signals_container); + scroll->setWidgetResizable(true); + scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + main_layout->addWidget(scroll); + + // history log + history_log = new HistoryLog(this); + main_layout->addWidget(history_log); + + QObject::connect(edit_btn, &QPushButton::clicked, this, &DetailWidget::editMsg); + QObject::connect(binary_view, &BinaryView::cellsSelected, this, &DetailWidget::addSignal); + QObject::connect(can, &CANMessages::updated, this, &DetailWidget::updateState); + QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &DetailWidget::dbcMsgChanged); +} + +void DetailWidget::setMessage(const QString &message_id) { + if (msg_id != message_id) { + msg_id = message_id; + dbcMsgChanged(); + } +} + +void DetailWidget::dbcMsgChanged() { + if (msg_id.isEmpty()) return; + + qDeleteAll(signals_container->findChildren()); + QString msg_name = tr("untitled"); + if (auto msg = dbc()->msg(msg_id)) { + for (int i = 0; i < msg->sigs.size(); ++i) { + auto form = new SignalEdit(i, msg_id, msg->sigs[i]); + signals_container->layout()->addWidget(form); + QObject::connect(form, &SignalEdit::showChart, [this, sig = &msg->sigs[i]]() { emit showChart(msg_id, sig); }); + QObject::connect(form, &SignalEdit::showFormClicked, this, &DetailWidget::showForm); + QObject::connect(form, &SignalEdit::remove, this, &DetailWidget::removeSignal); + QObject::connect(form, &SignalEdit::save, this, &DetailWidget::saveSignal); + } + msg_name = msg->name.c_str(); + } + edit_btn->setVisible(true); + name_label->setText(msg_name); + + binary_view->setMessage(msg_id); + history_log->setMessage(msg_id); +} + +void DetailWidget::updateState() { + time_label->setText(QString::number(can->currentSec(), 'f', 3)); + if (msg_id.isEmpty()) return; + + binary_view->updateState(); + history_log->updateState(); +} + +void DetailWidget::showForm() { + SignalEdit *sender = qobject_cast(QObject::sender()); + for (auto f : signals_container->findChildren()) { + f->setFormVisible(f == sender && !f->isFormVisible()); + if (f == sender) { + QTimer::singleShot(0, [=]() { scroll->ensureWidgetVisible(f); }); + } + } +} + +void DetailWidget::editMsg() { + auto msg = dbc()->msg(msg_id); + QString name = msg ? msg->name.c_str() : "untitled"; + int size = msg ? msg->size : can->lastMessage(msg_id).dat.size(); + EditMessageDialog dlg(msg_id, name, size, this); + if (dlg.exec()) { + dbc()->updateMsg(msg_id, dlg.name_edit->text(), dlg.size_spin->value()); + dbcMsgChanged(); + } +} + +void DetailWidget::addSignal(int start_bit, int size) { + if (dbc()->msg(msg_id)) { + AddSignalDialog dlg(msg_id, start_bit, size, this); + if (dlg.exec()) { + dbc()->addSignal(msg_id, dlg.form->getSignal()); + dbcMsgChanged(); + } + } +} + +void DetailWidget::saveSignal() { + SignalEdit *sig_form = qobject_cast(QObject::sender()); + auto s = sig_form->form->getSignal(); + dbc()->updateSignal(msg_id, sig_form->sig_name, s); + // update binary view and history log + binary_view->setMessage(msg_id); + history_log->setMessage(msg_id); +} + +void DetailWidget::removeSignal() { + SignalEdit *sig_form = qobject_cast(QObject::sender()); + QString text = tr("Are you sure you want to remove signal '%1'").arg(sig_form->sig_name); + if (QMessageBox::Yes == QMessageBox::question(this, tr("Remove signal"), text)) { + dbc()->removeSignal(msg_id, sig_form->sig_name); + dbcMsgChanged(); + } +} + +// BinaryView + +BinaryView::BinaryView(QWidget *parent) : QTableWidget(parent) { + horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + verticalHeader()->setSectionResizeMode(QHeaderView::Stretch); + horizontalHeader()->hide(); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setColumnCount(9); + + // replace selection model + auto old_model = selectionModel(); + setSelectionModel(new BinarySelectionModel(model())); + delete old_model; +} + +void BinaryView::mouseReleaseEvent(QMouseEvent *event) { + QTableWidget::mouseReleaseEvent(event); + + if (auto items = selectedItems(); !items.isEmpty()) { + int start_bit = items.first()->row() * 8 + items.first()->column(); + int size = items.back()->row() * 8 + items.back()->column() - start_bit + 1; + emit cellsSelected(start_bit, size); + } +} + +void BinaryView::setMessage(const QString &message_id) { + msg_id = message_id; + if (msg_id.isEmpty()) return; + + const Msg *msg = dbc()->msg(msg_id); + int row_count = msg ? msg->size : can->lastMessage(msg_id).dat.size(); + setRowCount(row_count); + setColumnCount(9); + for (int i = 0; i < rowCount(); ++i) { + for (int j = 0; j < columnCount(); ++j) { + auto item = new QTableWidgetItem(); + item->setFlags(item->flags() ^ Qt::ItemIsEditable); + item->setTextAlignment(Qt::AlignCenter); + if (j == 8) { + QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont); + font.setBold(true); + item->setFont(font); + item->setFlags(item->flags() ^ Qt::ItemIsSelectable); + } + setItem(i, j, item); + } + } + + // set background color + if (msg) { + for (int i = 0; i < msg->sigs.size(); ++i) { + const auto &sig = msg->sigs[i]; + int start = sig.is_little_endian ? sig.start_bit : bigEndianBitIndex(sig.start_bit); + for (int j = start; j <= std::min(start + sig.size - 1, rowCount() * columnCount() - 1); ++j) { + item(j / 8, j % 8)->setBackground(QColor(getColor(i))); + } + } + } + + setFixedHeight(rowHeight(0) * std::min(row_count, 8) + 2); + clearSelection(); + updateState(); +} + +void BinaryView::updateState() { + const auto &binary = can->lastMessage(msg_id).dat; + setUpdatesEnabled(false); + char hex[3] = {'\0'}; + for (int i = 0; i < binary.size(); ++i) { + for (int j = 0; j < 8; ++j) { + item(i, j)->setText(QChar((binary[i] >> (7 - j)) & 1 ? '1' : '0')); + } + hex[0] = toHex(binary[i] >> 4); + hex[1] = toHex(binary[i] & 0xf); + item(i, 8)->setText(hex); + } + setUpdatesEnabled(true); +} + +void BinarySelectionModel::select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command) { + QItemSelection new_selection = selection; + if (auto indexes = selection.indexes(); !indexes.isEmpty()) { + auto [begin_idx, end_idx] = (QModelIndex[]){indexes.first(), indexes.back()}; + for (int row = begin_idx.row(); row <= end_idx.row(); ++row) { + int left_col = (row == begin_idx.row()) ? begin_idx.column() : 0; + int right_col = (row == end_idx.row()) ? end_idx.column() : 7; + new_selection.merge({model()->index(row, left_col), model()->index(row, right_col)}, command); + } + } + QItemSelectionModel::select(new_selection, command); +} + +// EditMessageDialog + +EditMessageDialog::EditMessageDialog(const QString &msg_id, const QString &title, int size, QWidget *parent) : QDialog(parent) { + setWindowTitle(tr("Edit message")); + QVBoxLayout *main_layout = new QVBoxLayout(this); + + QFormLayout *form_layout = new QFormLayout(); + form_layout->addRow("ID", new QLabel(msg_id)); + + name_edit = new QLineEdit(title, this); + form_layout->addRow(tr("Name"), name_edit); + + size_spin = new QSpinBox(this); + // TODO: limit the maximum? + size_spin->setMinimum(1); + size_spin->setValue(size); + form_layout->addRow(tr("Size"), size_spin); + + main_layout->addLayout(form_layout); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + main_layout->addWidget(buttonBox); + setFixedWidth(parent->width() * 0.9); + + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +// ScrollArea + +bool ScrollArea::eventFilter(QObject *obj, QEvent *ev) { + if (obj == widget() && ev->type() == QEvent::Resize) { + int height = widget()->height() + 4; + setMinimumHeight(height > 480 ? 480 : height); + setMaximumHeight(height); + } + return QScrollArea::eventFilter(obj, ev); +} + +void ScrollArea::setWidget(QWidget *w) { + QScrollArea::setWidget(w); + w->installEventFilter(this); +} diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h new file mode 100644 index 0000000000..db174873f7 --- /dev/null +++ b/tools/cabana/detailwidget.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include + +#include "opendbc/can/common.h" +#include "opendbc/can/common_dbc.h" +#include "tools/cabana/canmessages.h" +#include "tools/cabana/dbcmanager.h" +#include "tools/cabana/historylog.h" +#include "tools/cabana/signaledit.h" + +class BinarySelectionModel : public QItemSelectionModel { +public: + BinarySelectionModel(QAbstractItemModel *model = nullptr) : QItemSelectionModel(model) {} + void select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command) override; +}; + +class BinaryView : public QTableWidget { + Q_OBJECT +public: + BinaryView(QWidget *parent = nullptr); + void mouseReleaseEvent(QMouseEvent *event) override; + void setMessage(const QString &message_id); + void updateState(); +signals: + void cellsSelected(int start_bit, int size); + +private: + QString msg_id; +}; + +class EditMessageDialog : public QDialog { + Q_OBJECT + +public: + EditMessageDialog(const QString &msg_id, const QString &title, int size, QWidget *parent); + + QLineEdit *name_edit; + QSpinBox *size_spin; +}; + +class ScrollArea : public QScrollArea { + Q_OBJECT + +public: + ScrollArea(QWidget *parent) : QScrollArea(parent) {} + bool eventFilter(QObject *obj, QEvent *ev) override; + void setWidget(QWidget *w); +}; + +class DetailWidget : public QWidget { + Q_OBJECT + +public: + DetailWidget(QWidget *parent); + void setMessage(const QString &message_id); + void dbcMsgChanged(); + +signals: + void showChart(const QString &msg_id, const Signal *sig); + void removeChart(const Signal *sig); + +private: + void addSignal(int start_bit, int size); + void saveSignal(); + void removeSignal(); + void editMsg(); + void showForm(); + void updateState(); + + QString msg_id; + QLabel *name_label, *time_label; + QPushButton *edit_btn; + QWidget *signals_container; + HistoryLog *history_log; + BinaryView *binary_view; + ScrollArea *scroll; +}; diff --git a/tools/cabana/historylog.cc b/tools/cabana/historylog.cc new file mode 100644 index 0000000000..cbb3b6e882 --- /dev/null +++ b/tools/cabana/historylog.cc @@ -0,0 +1,95 @@ +#include "tools/cabana/historylog.h" + +#include +#include +#include + +QVariant HistoryLogModel::data(const QModelIndex &index, int role) const { + auto msg = dbc()->msg(msg_id); + if (role == Qt::DisplayRole) { + const auto &can_msgs = can->messages(msg_id); + if (index.row() < can_msgs.size()) { + const auto &can_data = can_msgs[index.row()]; + if (msg && index.column() < msg->sigs.size()) { + return get_raw_value((uint8_t *)can_data.dat.begin(), can_data.dat.size(), msg->sigs[index.column()]); + } else { + return toHex(can_data.dat); + } + } + } else if (role == Qt::FontRole) { + if (index.column() == 0 && !(msg && msg->sigs.size() > 0)) { + return QFontDatabase::systemFont(QFontDatabase::FixedFont); + } + } + return {}; +} + +void HistoryLogModel::setMessage(const QString &message_id) { + beginResetModel(); + msg_id = message_id; + const auto msg = dbc()->msg(message_id); + column_count = msg && !msg->sigs.empty() ? msg->sigs.size() : 1; + row_count = 0; + endResetModel(); + + updateState(); +} + +QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (orientation == Qt::Horizontal) { + auto msg = dbc()->msg(msg_id); + if (msg && section < msg->sigs.size()) { + if (role == Qt::BackgroundRole) { + return QBrush(QColor(getColor(section))); + } else if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { + return QString::fromStdString(msg->sigs[section].name); + } + } + } else if (role == Qt::DisplayRole) { + const auto &can_msgs = can->messages(msg_id); + if (section < can_msgs.size()) { + return QString::number(can_msgs[section].ts, 'f', 2); + } + } + return {}; +} + +void HistoryLogModel::updateState() { + if (msg_id.isEmpty()) return; + + int prev_row_count = row_count; + row_count = can->messages(msg_id).size(); + int delta = row_count - prev_row_count; + if (delta > 0) { + beginInsertRows({}, prev_row_count, row_count - 1); + endInsertRows(); + } else if (delta < 0) { + beginRemoveRows({}, row_count, prev_row_count - 1); + endRemoveRows(); + } + if (row_count > 0) { + emit dataChanged(index(0, 0), index(row_count - 1, column_count - 1), {Qt::DisplayRole}); + emit headerDataChanged(Qt::Vertical, 0, row_count - 1); + } +} + +HistoryLog::HistoryLog(QWidget *parent) : QWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + model = new HistoryLogModel(this); + table = new QTableView(this); + table->setModel(model); + table->horizontalHeader()->setStretchLastSection(true); + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + table->setStyleSheet("QTableView::item { border:0px; padding-left:5px; padding-right:5px; }"); + table->verticalHeader()->setStyleSheet("QHeaderView::section {padding-left: 5px; padding-right: 5px;min-width:40px;}"); + main_layout->addWidget(table); +} + +void HistoryLog::setMessage(const QString &message_id) { + model->setMessage(message_id); +} + +void HistoryLog::updateState() { + model->updateState(); +} diff --git a/tools/cabana/historylog.h b/tools/cabana/historylog.h new file mode 100644 index 0000000000..f3a9046bfa --- /dev/null +++ b/tools/cabana/historylog.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +#include "tools/cabana/canmessages.h" +#include "tools/cabana/dbcmanager.h" + +class HistoryLogModel : public QAbstractTableModel { +Q_OBJECT + +public: + HistoryLogModel(QObject *parent) : QAbstractTableModel(parent) {} + void setMessage(const QString &message_id); + void updateState(); + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override { return column_count; } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; } + +private: + QString msg_id; + int row_count = 0; + int column_count = 0; +}; + +class HistoryLog : public QWidget { + Q_OBJECT + +public: + HistoryLog(QWidget *parent); + void setMessage(const QString &message_id); + void updateState(); + +private: + QTableView *table; + HistoryLogModel *model; +}; diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc new file mode 100644 index 0000000000..c9d9c85141 --- /dev/null +++ b/tools/cabana/mainwin.cc @@ -0,0 +1,115 @@ +#include "tools/cabana/mainwin.h" + +#include +#include +#include +#include +#include +#include + +MainWindow::MainWindow() : QWidget() { + QVBoxLayout *main_layout = new QVBoxLayout(this); + + QHBoxLayout *h_layout = new QHBoxLayout(); + main_layout->addLayout(h_layout); + + messages_widget = new MessagesWidget(this); + h_layout->addWidget(messages_widget); + + detail_widget = new DetailWidget(this); + detail_widget->setFixedWidth(600); + h_layout->addWidget(detail_widget); + + // right widgets + QWidget *right_container = new QWidget(this); + right_container->setFixedWidth(640); + r_layout = new QVBoxLayout(right_container); + + QPushButton *settings_btn = new QPushButton("Settings"); + r_layout->addWidget(settings_btn, 0, Qt::AlignRight); + + video_widget = new VideoWidget(this); + r_layout->addWidget(video_widget, 0, Qt::AlignTop); + + charts_widget = new ChartsWidget(this); + r_layout->addWidget(charts_widget); + + h_layout->addWidget(right_container); + + QObject::connect(messages_widget, &MessagesWidget::msgSelectionChanged, detail_widget, &DetailWidget::setMessage); + QObject::connect(detail_widget, &DetailWidget::showChart, charts_widget, &ChartsWidget::addChart); + QObject::connect(charts_widget, &ChartsWidget::dock, this, &MainWindow::dockCharts); + QObject::connect(settings_btn, &QPushButton::clicked, this, &MainWindow::setOption); +} + +void MainWindow::dockCharts(bool dock) { + charts_widget->setUpdatesEnabled(false); + if (dock && floating_window) { + floating_window->removeEventFilter(charts_widget); + r_layout->addWidget(charts_widget); + floating_window->deleteLater(); + floating_window = nullptr; + } else if (!dock && !floating_window) { + floating_window = new QWidget(nullptr); + floating_window->setLayout(new QVBoxLayout()); + floating_window->layout()->addWidget(charts_widget); + floating_window->installEventFilter(charts_widget); + floating_window->setMinimumSize(QGuiApplication::primaryScreen()->size() / 2); + floating_window->showMaximized(); + } + charts_widget->setUpdatesEnabled(true); +} + +void MainWindow::closeEvent(QCloseEvent *event) { + if (floating_window) + floating_window->deleteLater(); + QWidget::closeEvent(event); +} + +void MainWindow::setOption() { + SettingsDlg dlg(this); + dlg.exec(); +} + +// SettingsDlg + +SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { + setWindowTitle(tr("Settings")); + QVBoxLayout *main_layout = new QVBoxLayout(this); + QFormLayout *form_layout = new QFormLayout(); + + fps = new QSpinBox(this); + fps->setRange(10, 100); + fps->setSingleStep(10); + fps->setValue(settings.fps); + form_layout->addRow("FPS", fps); + + log_size = new QSpinBox(this); + log_size->setRange(50, 500); + log_size->setSingleStep(10); + log_size->setValue(settings.can_msg_log_size); + form_layout->addRow(tr("Log size"), log_size); + + cached_segment = new QSpinBox(this); + cached_segment->setRange(3, 60); + cached_segment->setSingleStep(1); + cached_segment->setValue(settings.cached_segment_limit); + form_layout->addRow(tr("Cached segments limit"), cached_segment); + + main_layout->addLayout(form_layout); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + main_layout->addWidget(buttonBox); + + setFixedWidth(360); + connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDlg::save); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +void SettingsDlg::save() { + settings.fps = fps->value(); + settings.can_msg_log_size = log_size->value(); + settings.cached_segment_limit = cached_segment->value(); + settings.save(); + accept(); +} diff --git a/tools/cabana/mainwin.h b/tools/cabana/mainwin.h new file mode 100644 index 0000000000..14c4b1dfa9 --- /dev/null +++ b/tools/cabana/mainwin.h @@ -0,0 +1,36 @@ +#pragma once + +#include "tools/cabana/chartswidget.h" +#include "tools/cabana/detailwidget.h" +#include "tools/cabana/messageswidget.h" +#include "tools/cabana/videowidget.h" + +class MainWindow : public QWidget { + Q_OBJECT + +public: + MainWindow(); + void dockCharts(bool dock); + +protected: + void closeEvent(QCloseEvent *event) override; + void setOption(); + + VideoWidget *video_widget; + MessagesWidget *messages_widget; + DetailWidget *detail_widget; + ChartsWidget *charts_widget; + QWidget *floating_window = nullptr; + QVBoxLayout *r_layout; +}; + +class SettingsDlg : public QDialog { + Q_OBJECT + +public: + SettingsDlg(QWidget *parent); + void save(); + QSpinBox *fps; + QSpinBox *log_size ; + QSpinBox *cached_segment; +}; diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc new file mode 100644 index 0000000000..3c9af67ea6 --- /dev/null +++ b/tools/cabana/messageswidget.cc @@ -0,0 +1,134 @@ +#include "tools/cabana/messageswidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/dbcmanager.h" + +MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + + // DBC file selector + QHBoxLayout *dbc_file_layout = new QHBoxLayout(); + QComboBox *combo = new QComboBox(this); + auto dbc_names = dbc()->allDBCNames(); + for (const auto &name : dbc_names) { + combo->addItem(QString::fromStdString(name)); + } + combo->setEditable(true); + combo->setCurrentText(QString()); + combo->setInsertPolicy(QComboBox::NoInsert); + combo->completer()->setCompletionMode(QCompleter::PopupCompletion); + QFont font; + font.setBold(true); + combo->lineEdit()->setFont(font); + dbc_file_layout->addWidget(combo); + + dbc_file_layout->addStretch(); + QPushButton *save_btn = new QPushButton(tr("Save DBC"), this); + dbc_file_layout->addWidget(save_btn); + main_layout->addLayout(dbc_file_layout); + + // message filter + QLineEdit *filter = new QLineEdit(this); + filter->setPlaceholderText(tr("filter messages")); + main_layout->addWidget(filter); + + // message table + table_widget = new QTableView(this); + model = new MessageListModel(this); + QSortFilterProxyModel *proxy_model = new QSortFilterProxyModel(this); + proxy_model->setSourceModel(model); + proxy_model->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxy_model->setDynamicSortFilter(false); + table_widget->setModel(proxy_model); + table_widget->setSelectionBehavior(QAbstractItemView::SelectRows); + table_widget->setSelectionMode(QAbstractItemView::SingleSelection); + table_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + table_widget->setSortingEnabled(true); + table_widget->setColumnWidth(0, 250); + table_widget->setColumnWidth(1, 80); + table_widget->setColumnWidth(2, 80); + table_widget->horizontalHeader()->setStretchLastSection(true); + table_widget->verticalHeader()->hide(); + table_widget->sortByColumn(0, Qt::AscendingOrder); + main_layout->addWidget(table_widget); + + // signals/slots + QObject::connect(filter, &QLineEdit::textChanged, proxy_model, &QSortFilterProxyModel::setFilterFixedString); + QObject::connect(can, &CANMessages::updated, model, &MessageListModel::updateState); + QObject::connect(combo, SIGNAL(activated(const QString &)), SLOT(dbcSelectionChanged(const QString &))); + QObject::connect(save_btn, &QPushButton::clicked, [=]() { + // TODO: save DBC to file + }); + QObject::connect(table_widget->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex ¤t, const QModelIndex &previous) { + if (current.isValid()) { + emit msgSelectionChanged(current.data(Qt::UserRole).toString()); + } + }); + + // For test purpose + combo->setCurrentText("toyota_nodsu_pt_generated"); +} + +void MessagesWidget::dbcSelectionChanged(const QString &dbc_file) { + dbc()->open(dbc_file); + // TODO: reset model? + table_widget->sortByColumn(0, Qt::AscendingOrder); +} + +// MessageListModel + +QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) + return (QString[]){"Name", "ID", "Count", "Bytes"}[section]; + return {}; +} + +QVariant MessageListModel::data(const QModelIndex &index, int role) const { + if (role == Qt::DisplayRole) { + auto it = std::next(can->can_msgs.begin(), index.row()); + if (it != can->can_msgs.end() && !it.value().empty()) { + const QString &msg_id = it.key(); + switch (index.column()) { + case 0: { + auto msg = dbc()->msg(msg_id); + return msg ? msg->name.c_str() : "untitled"; + } + case 1: return msg_id; + case 2: return can->counters[msg_id]; + case 3: return toHex(it.value().front().dat); + } + } + } else if (role == Qt::UserRole) { + return std::next(can->can_msgs.begin(), index.row()).key(); + } else if (role == Qt::FontRole) { + if (index.column() == 3) { + return QFontDatabase::systemFont(QFontDatabase::FixedFont); + } + } + return {}; +} + +void MessageListModel::updateState() { + int prev_row_count = row_count; + row_count = can->can_msgs.size(); + int delta = row_count - prev_row_count; + if (delta > 0) { + beginInsertRows({}, prev_row_count, row_count - 1); + endInsertRows(); + } else if (delta < 0) { + beginRemoveRows({}, row_count, prev_row_count - 1); + endRemoveRows(); + } + + if (row_count > 0) { + emit dataChanged(index(0, 0), index(row_count - 1, 3), {Qt::DisplayRole}); + } +} diff --git a/tools/cabana/messageswidget.h b/tools/cabana/messageswidget.h new file mode 100644 index 0000000000..f6487ba2bd --- /dev/null +++ b/tools/cabana/messageswidget.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +#include "tools/cabana/canmessages.h" + +class MessageListModel : public QAbstractTableModel { +Q_OBJECT + +public: + MessageListModel(QObject *parent) : QAbstractTableModel(parent) {} + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 4; } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; } + void updateState(); + +private: + int row_count = 0; +}; + +class MessagesWidget : public QWidget { + Q_OBJECT + +public: + MessagesWidget(QWidget *parent); + +public slots: + void dbcSelectionChanged(const QString &dbc_file); + +signals: + void msgSelectionChanged(const QString &message_id); + +protected: + QTableView *table_widget; + MessageListModel *model; +}; diff --git a/tools/cabana/signaledit.cc b/tools/cabana/signaledit.cc new file mode 100644 index 0000000000..2da3e2eec2 --- /dev/null +++ b/tools/cabana/signaledit.cc @@ -0,0 +1,159 @@ +#include "tools/cabana/signaledit.h" + +#include +#include +#include +#include + +// SignalForm + +SignalForm::SignalForm(const Signal &sig, QWidget *parent) : start_bit(sig.start_bit), QWidget(parent) { + QFormLayout *form_layout = new QFormLayout(this); + + name = new QLineEdit(sig.name.c_str()); + form_layout->addRow(tr("Name"), name); + + size = new QSpinBox(); + size->setMinimum(1); + size->setValue(sig.size); + form_layout->addRow(tr("Size"), size); + + endianness = new QComboBox(); + endianness->addItems({"Little", "Big"}); + endianness->setCurrentIndex(sig.is_little_endian ? 0 : 1); + form_layout->addRow(tr("Endianness"), endianness); + + sign = new QComboBox(); + sign->addItems({"Signed", "Unsigned"}); + sign->setCurrentIndex(sig.is_signed ? 0 : 1); + form_layout->addRow(tr("sign"), sign); + + factor = new QDoubleSpinBox(); + factor->setDecimals(3); + factor->setValue(sig.factor); + form_layout->addRow(tr("Factor"), factor); + + offset = new QSpinBox(); + offset->setValue(sig.offset); + form_layout->addRow(tr("Offset"), offset); + + // TODO: parse the following parameters in opendbc + unit = new QLineEdit(); + form_layout->addRow(tr("Unit"), unit); + comment = new QLineEdit(); + form_layout->addRow(tr("Comment"), comment); + min_val = new QDoubleSpinBox(); + factor->setDecimals(3); + form_layout->addRow(tr("Minimum value"), min_val); + max_val = new QDoubleSpinBox(); + factor->setDecimals(3); + form_layout->addRow(tr("Maximum value"), max_val); + val_desc = new QLineEdit(); + form_layout->addRow(tr("Value descriptions"), val_desc); +} + +Signal SignalForm::getSignal() { + // TODO: Check if the size is valid, and no duplicate name + Signal sig = {}; + sig.start_bit = start_bit; + sig.name = name->text().toStdString(); + sig.size = size->text().toInt(); + sig.offset = offset->text().toDouble(); + sig.factor = factor->text().toDouble(); + sig.is_signed = sign->currentIndex() == 0; + sig.is_little_endian = endianness->currentIndex() == 0; + if (sig.is_little_endian) { + sig.lsb = sig.start_bit; + sig.msb = sig.start_bit + sig.size - 1; + } else { + sig.lsb = bigEndianStartBitsIndex(bigEndianBitIndex(sig.start_bit) + sig.size - 1); + sig.msb = sig.start_bit; + } + return sig; +} + +// SignalEdit + +SignalEdit::SignalEdit(int index, const QString &msg_id, const Signal &sig, QWidget *parent) + : sig_name(sig.name.c_str()), QWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + + // title bar + QHBoxLayout *title_layout = new QHBoxLayout(); + icon = new QLabel(">"); + icon->setFixedSize(15, 30); + icon->setStyleSheet("font-weight:bold"); + title_layout->addWidget(icon); + title = new ElidedLabel(this); + title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); + title->setText(QString("%1. %2").arg(index + 1).arg(sig_name)); + title->setStyleSheet(QString("font-weight:bold; color:%1").arg(getColor(index))); + title_layout->addWidget(title); + + QPushButton *plot_btn = new QPushButton("📈"); + plot_btn->setToolTip(tr("Show Plot")); + plot_btn->setFixedSize(30, 30); + QObject::connect(plot_btn, &QPushButton::clicked, this, &SignalEdit::showChart); + title_layout->addWidget(plot_btn); + main_layout->addLayout(title_layout); + + // signal form + form_container = new QWidget(this); + QVBoxLayout *v_layout = new QVBoxLayout(form_container); + form = new SignalForm(sig, this); + v_layout->addWidget(form); + + QHBoxLayout *h = new QHBoxLayout(); + QPushButton *remove_btn = new QPushButton(tr("Remove Signal")); + h->addWidget(remove_btn); + h->addStretch(); + QPushButton *save_btn = new QPushButton(tr("Save")); + h->addWidget(save_btn); + v_layout->addLayout(h); + + form_container->setVisible(false); + main_layout->addWidget(form_container); + + // bottom line + QFrame *hline = new QFrame(); + hline->setFrameShape(QFrame::HLine); + hline->setFrameShadow(QFrame::Sunken); + main_layout->addWidget(hline); + + QObject::connect(remove_btn, &QPushButton::clicked, this, &SignalEdit::remove); + QObject::connect(save_btn, &QPushButton::clicked, [=]() { + QString new_name = form->getSignal().name.c_str(); + title->setText(QString("%1. %2").arg(index + 1).arg(new_name)); + emit save(); + sig_name = new_name; + }); + QObject::connect(title, &ElidedLabel::clicked, this, &SignalEdit::showFormClicked); +} + +void SignalEdit::setFormVisible(bool visible) { + form_container->setVisible(visible); + icon->setText(visible ? "▼" : ">"); +} + +// AddSignalDialog + +AddSignalDialog::AddSignalDialog(const QString &id, int start_bit, int size, QWidget *parent) : QDialog(parent) { + setWindowTitle(tr("Add signal to %1").arg(dbc()->msg(id)->name.c_str())); + QVBoxLayout *main_layout = new QVBoxLayout(this); + + Signal sig = { + .name = "untitled", + .start_bit = bigEndianBitIndex(start_bit), + .is_little_endian = false, + .size = size, + }; + form = new SignalForm(sig, this); + main_layout->addWidget(form); + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + main_layout->addWidget(buttonBox); + setFixedWidth(parent->width() * 0.9); + + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); +} diff --git a/tools/cabana/signaledit.h b/tools/cabana/signaledit.h new file mode 100644 index 0000000000..f31408657f --- /dev/null +++ b/tools/cabana/signaledit.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "selfdrive/ui/qt/widgets/controls.h" + +#include "tools/cabana/canmessages.h" +#include "tools/cabana/dbcmanager.h" + +class SignalForm : public QWidget { +public: + SignalForm(const Signal &sig, QWidget *parent); + Signal getSignal(); + + QLineEdit *name, *unit, *comment, *val_desc; + QSpinBox *size, *offset; + QDoubleSpinBox *factor, *min_val, *max_val; + QComboBox *sign, *endianness; + int start_bit = 0; +}; + +class SignalEdit : public QWidget { + Q_OBJECT + +public: + SignalEdit(int index, const QString &msg_id, const Signal &sig, QWidget *parent = nullptr); + void setFormVisible(bool show); + inline bool isFormVisible() const { return form_container->isVisible(); } + QString sig_name; + SignalForm *form; + +signals: + void showChart(); + void showFormClicked(); + void remove(); + void save(); + +protected: + ElidedLabel *title; + QWidget *form_container; + QLabel *icon; +}; + +class AddSignalDialog : public QDialog { +public: + AddSignalDialog(const QString &id, int start_bit, int size, QWidget *parent); + SignalForm *form; +}; diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc new file mode 100644 index 0000000000..b6fe8de3e2 --- /dev/null +++ b/tools/cabana/videowidget.cc @@ -0,0 +1,148 @@ +#include "tools/cabana/videowidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +inline QString formatTime(int seconds) { + return QDateTime::fromTime_t(seconds).toString(seconds > 60 * 60 ? "hh:mm:ss" : "mm:ss"); +} + +VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + + // TODO: figure out why the CameraViewWidget crashed occasionally. + cam_widget = new CameraViewWidget("camerad", VISION_STREAM_ROAD, false, this); + cam_widget->setFixedSize(parent->width(), parent->width() / 1.596); + main_layout->addWidget(cam_widget); + + // slider controls + QHBoxLayout *slider_layout = new QHBoxLayout(); + QLabel *time_label = new QLabel("00:00"); + slider_layout->addWidget(time_label); + + slider = new Slider(this); + slider->setSingleStep(0); + slider->setMinimum(0); + slider->setMaximum(can->totalSeconds() * 1000); + slider_layout->addWidget(slider); + + end_time_label = new QLabel(formatTime(can->totalSeconds())); + slider_layout->addWidget(end_time_label); + main_layout->addLayout(slider_layout); + + // btn controls + QHBoxLayout *control_layout = new QHBoxLayout(); + play_btn = new QPushButton("⏸"); + play_btn->setStyleSheet("font-weight:bold"); + control_layout->addWidget(play_btn); + + QButtonGroup *group = new QButtonGroup(this); + group->setExclusive(true); + for (float speed : {0.1, 0.5, 1., 2.}) { + QPushButton *btn = new QPushButton(QString("%1x").arg(speed), this); + btn->setCheckable(true); + QObject::connect(btn, &QPushButton::clicked, [=]() { can->setSpeed(speed); }); + control_layout->addWidget(btn); + group->addButton(btn); + if (speed == 1.0) btn->setChecked(true); + } + main_layout->addLayout(control_layout); + + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + QObject::connect(can, &CANMessages::rangeChanged, this, &VideoWidget::rangeChanged); + QObject::connect(can, &CANMessages::updated, this, &VideoWidget::updateState); + QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->value() / 1000.0); }); + QObject::connect(slider, &QSlider::valueChanged, [=](int value) { time_label->setText(formatTime(value / 1000)); }); + QObject::connect(cam_widget, &CameraViewWidget::clicked, [this]() { pause(!can->isPaused()); }); + QObject::connect(play_btn, &QPushButton::clicked, [=]() { pause(!can->isPaused()); }); +} + +void VideoWidget::pause(bool pause) { + play_btn->setText(!pause ? "⏸" : "▶"); + can->pause(pause); +} + +void VideoWidget::rangeChanged(double min, double max) { + if (!can->isZoomed()) { + min = 0; + max = can->totalSeconds(); + } + end_time_label->setText(formatTime(max)); + slider->setMinimum(min * 1000); + slider->setMaximum(max * 1000); +} + +void VideoWidget::updateState() { + if (!slider->isSliderDown()) + slider->setValue(can->currentSec() * 1000); +} + +// Slider +Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) { + QTimer *timer = new QTimer(this); + timer->setInterval(2000); + timer->callOnTimeout([this]() { + timeline = can->getTimeline(); + update(); + }); + timer->start(); +} + +void Slider::sliderChange(QAbstractSlider::SliderChange change) { + if (change == QAbstractSlider::SliderValueChange) { + int x = width() * ((value() - minimum()) / double(maximum() - minimum())); + if (x != slider_x) { + slider_x = x; + update(); + } + } else { + QAbstractSlider::sliderChange(change); + } +} + +void Slider::paintEvent(QPaintEvent *ev) { + static const QColor colors[] = { + [(int)TimelineType::None] = QColor(111, 143, 175), + [(int)TimelineType::Engaged] = QColor(0, 163, 108), + [(int)TimelineType::UserFlag] = Qt::white, + [(int)TimelineType::AlertInfo] = Qt::green, + [(int)TimelineType::AlertWarning] = QColor(255, 195, 0), + [(int)TimelineType::AlertCritical] = QColor(199, 0, 57)}; + + QPainter p(this); + QRect r = rect().adjusted(0, 4, 0, -4); + p.fillRect(r, colors[(int)TimelineType::None]); + double min = minimum() / 1000.0; + double max = maximum() / 1000.0; + for (auto [begin, end, type] : timeline) { + if (begin > max || end < min) + continue; + r.setLeft(((std::max(min, (double)begin) - min) / (max - min)) * width()); + r.setRight(((std::min(max, (double)end) - min) / (max - min)) * width()); + p.fillRect(r, colors[(int)type]); + } + + QStyleOptionSlider opt; + opt.initFrom(this); + opt.minimum = minimum(); + opt.maximum = maximum(); + opt.subControls = QStyle::SC_SliderHandle; + opt.sliderPosition = value(); + style()->drawComplexControl(QStyle::CC_Slider, &opt, &p); +} + +void Slider::mousePressEvent(QMouseEvent *e) { + QSlider::mousePressEvent(e); + if (e->button() == Qt::LeftButton && !isSliderDown()) { + int value = minimum() + ((maximum() - minimum()) * e->x()) / width(); + setValue(value); + emit sliderReleased(); + } +} diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h new file mode 100644 index 0000000000..fd896f1e11 --- /dev/null +++ b/tools/cabana/videowidget.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +#include "selfdrive/ui/qt/widgets/cameraview.h" +#include "tools/cabana/canmessages.h" + +class Slider : public QSlider { + Q_OBJECT + +public: + Slider(QWidget *parent); + void mousePressEvent(QMouseEvent *e) override; + void sliderChange(QAbstractSlider::SliderChange change) override; + void paintEvent(QPaintEvent *ev) override; + + int slider_x = -1; + std::vector> timeline; +}; + +class VideoWidget : public QWidget { + Q_OBJECT + +public: + VideoWidget(QWidget *parnet = nullptr); + +protected: + void rangeChanged(double min, double max); + void updateState(); + void pause(bool pause); + + CameraViewWidget *cam_widget; + QLabel *end_time_label; + QPushButton *play_btn; + Slider *slider; +}; diff --git a/tools/camerastream/compressed_vipc.py b/tools/camerastream/compressed_vipc.py index 4322ce279a..42a416985e 100755 --- a/tools/camerastream/compressed_vipc.py +++ b/tools/camerastream/compressed_vipc.py @@ -85,6 +85,16 @@ def decoder(addr, sock_name, vipc_server, vst, nvidia): time_q = time_q[1:] print("%2d %4d %.3f %.3f roll %6.2f ms latency %6.2f ms + %6.2f ms + %6.2f ms = %6.2f ms" % (len(msgs), evta.idx.encodeId, evt.logMonoTime/1e9, evta.idx.timestampEof/1e6, frame_latency, process_latency, network_latency, pc_latency, process_latency+network_latency+pc_latency ), len(evta.data), sock_name) +def main(addr, cams, nvidia=False): + vipc_server = VisionIpcServer("camerad") + for vst in cams.values(): + vipc_server.create_buffers(vst, 4, False, W, H) + vipc_server.start_listener() + + for k, v in cams.items(): + multiprocessing.Process(target=decoder, args=(addr, k, vipc_server, v, nvidia)).start() + + if __name__ == "__main__": parser = argparse.ArgumentParser(description="Decode video streams and broadcast on VisionIPC") parser.add_argument("addr", help="Address of comma three") @@ -98,11 +108,4 @@ if __name__ == "__main__": ("driverEncodeData", VisionStreamType.VISION_STREAM_DRIVER), ] cams = dict([all_cams[int(x)] for x in args.cams.split(",")]) - - vipc_server = VisionIpcServer("camerad") - for vst in cams.values(): - vipc_server.create_buffers(vst, 4, False, W, H) - vipc_server.start_listener() - - for k,v in cams.items(): - multiprocessing.Process(target=decoder, args=(args.addr, k, vipc_server, v, args.nvidia)).start() + main(args.addr, cams, args.nvidia) diff --git a/tools/gpstest/.gitignore b/tools/gpstest/.gitignore new file mode 100644 index 0000000000..f11597286e --- /dev/null +++ b/tools/gpstest/.gitignore @@ -0,0 +1,2 @@ +LimeGPS/ +LimeSuite/ \ No newline at end of file diff --git a/tools/gpstest/README.md b/tools/gpstest/README.md new file mode 100644 index 0000000000..5aff0ee3d7 --- /dev/null +++ b/tools/gpstest/README.md @@ -0,0 +1,36 @@ +# GPS test setup +Testing the GPS receiver using GPS spoofing. At the moment only +static location relpay is supported. + +# Usage +``` +# on host, start gps signal simulation +./run_static_lime.py +``` + +`run_static_lime.py` downloads the latest ephemeris file from +https://cddis.nasa.gov/archive/gnss/data/daily/20xx/brdc/. + + +# Hardware Setup +* [LimeSDR USB](https://wiki.myriadrf.org/LimeSDR-USB) +* Asus AX58BT antenna + +# Software Setup +* https://github.com/myriadrf/LimeSuite +To communicate with LimeSDR the LimeSuite is needed it abstracts the direct +communication. It also contains examples for a quick start. + +The latest stable version (22.09) does not have the corresponding firmware +download available at https://downloads.myriadrf.org/project/limesuite. Therefore +version 20.10 was chosen. + +* https://github.com/osqzss/LimeGPS +Built on top of LimeSuite (libLimeSuite.so.20.10-1), generates the GPS signal. + +``` +./LimeGPS -e -l + +# Example +./LimeGPS -e /pathTo/brdc2660.22n -l 47.202028,15.740394,100 +``` diff --git a/tools/gpstest/fuzzy_testing.py b/tools/gpstest/fuzzy_testing.py new file mode 100755 index 0000000000..216e7d0dde --- /dev/null +++ b/tools/gpstest/fuzzy_testing.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +import sys +import time +import random +import datetime as dt +import subprocess as sp +import multiprocessing +import threading +from typing import Tuple, Any + +from laika.downloader import download_nav +from laika.gps_time import GPSTime +from laika.helpers import ConstellationId + +cache_dir = '/tmp/gpstest/' + + +def download_rinex(): + # TODO: check if there is a better way to get the full brdc file for LimeGPS + gps_time = GPSTime.from_datetime(dt.datetime.utcnow()) + utc_time = dt.datetime.utcnow() - dt.timedelta(1) + gps_time = GPSTime.from_datetime(dt.datetime(utc_time.year, utc_time.month, utc_time.day)) + return download_nav(gps_time, cache_dir, ConstellationId.GPS) + + +def exec_LimeGPS_bin(rinex_file: str, location: str, duration: int): + # this functions should never return, cause return means, timeout is + # reached or it crashed + try: + cmd = ["LimeGPS/LimeGPS", "-e", rinex_file, "-l", location] + sp.check_output(cmd, timeout=duration) + except sp.TimeoutExpired: + print("LimeGPS timeout reached!") + except Exception as e: + print(f"LimeGPS crashed: {str(e)}") + + +def run_lime_gps(rinex_file: str, location: str, duration: int): + print(f"LimeGPS {location} {duration}") + + p = multiprocessing.Process(target=exec_LimeGPS_bin, + args=(rinex_file, location, duration)) + p.start() + return p + + +def get_random_coords(lat, lon) -> Tuple[int, int]: + # jump around the world + # max values, lat: -90 to 90, lon: -180 to 180 + + lat_add = random.random()*20 + 10 + lon_add = random.random()*20 + 20 + + lat = ((lat + lat_add + 90) % 180) - 90 + lon = ((lon + lon_add + 180) % 360) - 180 + return round(lat, 5), round(lon, 5) + +def get_continuous_coords(lat, lon) -> Tuple[int, int]: + # continuously move around the world + + lat_add = random.random()*0.01 + lon_add = random.random()*0.01 + + lat = ((lat + lat_add + 90) % 180) - 90 + lon = ((lon + lon_add + 180) % 360) - 180 + return round(lat, 5), round(lon, 5) + +rc_p: Any = None +def exec_remote_checker(lat, lon, duration): + global rc_p + # TODO: good enough for testing + remote_cmd = "export PYTHONPATH=/data/pythonpath:/data/pythonpath/pyextra && " + remote_cmd += "cd /data/openpilot && " + remote_cmd += f"timeout {duration} /usr/local/pyenv/shims/python tools/gpstest/remote_checker.py " + remote_cmd += f"{lat} {lon}" + + ssh_cmd = ['ssh', '-i', '/home/batman/openpilot/xx/phone/key/id_rsa', + 'comma@192.168.60.130'] + ssh_cmd += [remote_cmd] + + rc_p = sp.Popen(ssh_cmd, stdout=sp.PIPE) + rc_p.wait() + rc_output = rc_p.stdout.read() + print(f"Checker Result: {rc_output.strip().decode('utf-8')}") + + +def run_remote_checker(spoof_proc, lat, lon, duration) -> bool: + checker_thread = threading.Thread(target=exec_remote_checker, args=(lat, lon, duration)) + checker_thread.start() + + tcnt = 0 + while True: + if not checker_thread.is_alive(): + # assume this only happens when the signal got matched + return True + + # the spoofing process has a timeout, kill checker if reached + if not spoof_proc.is_alive(): + rc_p.kill() + # spoofing process died, assume timeout + print("Spoofing process timeout") + return False + + print(f"Time elapsed: {tcnt}[s]", end = "\r") + time.sleep(1) + tcnt += 1 + + +def main(): + continuous_mode = False + if len(sys.argv) == 2 and sys.argv[1] == '-c': + print("Continuous Mode!") + continuous_mode = True + + rinex_file = download_rinex() + + duration = 60*3 # max runtime in seconds + lat, lon = get_random_coords(47.2020, 15.7403) + + while True: + # spoof random location + spoof_proc = run_lime_gps(rinex_file, f"{lat},{lon},100", duration) + start_time = time.monotonic() + + # remote checker runs blocking + if not run_remote_checker(spoof_proc, lat, lon, duration): + # location could not be matched by ublox module + pass + + end_time = time.monotonic() + spoof_proc.terminate() + + # -1 to count process startup + print(f"Time to get Signal: {round(end_time - start_time - 1, 4)}") + + if continuous_mode: + lat, lon = get_continuous_coords(lat, lon) + else: + lat, lon = get_random_coords(lat, lon) + +if __name__ == "__main__": + main() diff --git a/tools/gpstest/patches/limeGPS/inc_ephem_array_size.patch b/tools/gpstest/patches/limeGPS/inc_ephem_array_size.patch new file mode 100644 index 0000000000..9a3525d346 --- /dev/null +++ b/tools/gpstest/patches/limeGPS/inc_ephem_array_size.patch @@ -0,0 +1,13 @@ +diff --git a/gpssim.h b/gpssim.h +index c30b227..2ae0802 100644 +--- a/gpssim.h ++++ b/gpssim.h +@@ -75,7 +75,7 @@ + #define SC08 (8) + #define SC16 (16) + +-#define EPHEM_ARRAY_SIZE (13) // for daily GPS broadcast ephemers file (brdc) ++#define EPHEM_ARRAY_SIZE (20) // for daily GPS broadcast ephemers file (brdc) + + /*! \brief Structure representing GPS time */ + typedef struct diff --git a/tools/gpstest/patches/limeGPS/makefile.patch b/tools/gpstest/patches/limeGPS/makefile.patch new file mode 100644 index 0000000000..f99ce551db --- /dev/null +++ b/tools/gpstest/patches/limeGPS/makefile.patch @@ -0,0 +1,11 @@ +diff --git a/makefile b/makefile +index 51bfabf..d0ea1eb 100644 +--- a/makefile ++++ b/makefile +@@ -1,5 +1,4 @@ + CC=gcc -O2 -Wall + + all: limegps.c gpssim.c +- $(CC) -o LimeGPS limegps.c gpssim.c -lm -lpthread -lLimeSuite +- ++ $(CC) -o LimeGPS limegps.c gpssim.c -lm -lpthread -lLimeSuite -I../LimeSuite/src -L../LimeSuite/builddir/src -Wl,-rpath="$(PWD)/../LimeSuite/builddir/src" diff --git a/tools/gpstest/patches/limeSuite/mcu_error.patch b/tools/gpstest/patches/limeSuite/mcu_error.patch new file mode 100644 index 0000000000..91790a4a2b --- /dev/null +++ b/tools/gpstest/patches/limeSuite/mcu_error.patch @@ -0,0 +1,13 @@ +diff --git a/src/lms7002m/LMS7002M_RxTxCalibrations.cpp b/src/lms7002m/LMS7002M_RxTxCalibrations.cpp +index 41a37044..ac29c6b6 100644 +--- a/src/lms7002m/LMS7002M_RxTxCalibrations.cpp ++++ b/src/lms7002m/LMS7002M_RxTxCalibrations.cpp +@@ -254,7 +254,7 @@ int LMS7002M::CalibrateTx(float_type bandwidth_Hz, bool useExtLoopback) + mcuControl->RunProcedure(useExtLoopback ? MCU_FUNCTION_CALIBRATE_TX_EXTLOOPB : MCU_FUNCTION_CALIBRATE_TX); + status = mcuControl->WaitForMCU(1000); + if(status != MCU_BD::MCU_NO_ERROR) +- return ReportError(EINVAL, "Tx Calibration: MCU error %i (%s)", status, MCU_BD::MCUStatusMessage(status)); ++ return -1; //ReportError(EINVAL, "Tx Calibration: MCU error %i (%s)", status, MCU_BD::MCUStatusMessage(status)); + } + + //sync registers to cache diff --git a/tools/gpstest/patches/limeSuite/reference_print.patch b/tools/gpstest/patches/limeSuite/reference_print.patch new file mode 100644 index 0000000000..5bd7cdf1ed --- /dev/null +++ b/tools/gpstest/patches/limeSuite/reference_print.patch @@ -0,0 +1,13 @@ +diff --git a/src/FPGA_common/FPGA_common.cpp b/src/FPGA_common/FPGA_common.cpp +index 4e81f33e..7381c475 100644 +--- a/src/FPGA_common/FPGA_common.cpp ++++ b/src/FPGA_common/FPGA_common.cpp +@@ -946,7 +946,7 @@ double FPGA::DetectRefClk(double fx3Clk) + + if (i == 0) + return -1; +- lime::info("Reference clock %1.2f MHz", clkTbl[i - 1] / 1e6); ++ //lime::info("Reference clock %1.2f MHz", clkTbl[i - 1] / 1e6); + return clkTbl[i - 1]; + } + diff --git a/tools/gpstest/remote_checker.py b/tools/gpstest/remote_checker.py new file mode 100644 index 0000000000..84f6c0c3d9 --- /dev/null +++ b/tools/gpstest/remote_checker.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import sys +import time +from typing import List + +import cereal.messaging as messaging +from selfdrive.manager.process_config import managed_processes + +DELTA = 0.001 +# assume running openpilot for now +procs: List[str] = []#"ubloxd", "pigeond"] + + +def main(): + if len(sys.argv) < 3: + print("args: ") + return + + sol_lat = float(sys.argv[1]) + sol_lon = float(sys.argv[2]) + + for p in procs: + managed_processes[p].start() + time.sleep(0.5) # give time to startup + + gps_sock = messaging.sub_sock('gpsLocationExternal', timeout=0.1) + + # analyze until the location changed + while True: + events = messaging.drain_sock(gps_sock) + for e in events: + lat = e.gpsLocationExternal.latitude + lon = e.gpsLocationExternal.longitude + + if abs(lat - sol_lat) < DELTA and abs(lon - sol_lon) < DELTA: + print("MATCH") + return + + for p in procs: + if not managed_processes[p].proc.is_alive(): + print(f"ERROR: '{p}' died") + return + + +if __name__ == "__main__": + main() + for p in procs: + managed_processes[p].stop() diff --git a/tools/gpstest/run_static_gps_signal.py b/tools/gpstest/run_static_gps_signal.py new file mode 100755 index 0000000000..3787038f13 --- /dev/null +++ b/tools/gpstest/run_static_gps_signal.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +import os +import sys +import random +import datetime as dt +import subprocess as sp +from typing import Tuple + +from laika.downloader import download_nav +from laika.gps_time import GPSTime +from laika.helpers import ConstellationId + +cache_dir = '/tmp/gpstest/' + + +def download_rinex(): + # TODO: check if there is a better way to get the full brdc file for LimeGPS + gps_time = GPSTime.from_datetime(dt.datetime.utcnow()) + utc_time = dt.datetime.utcnow() - dt.timedelta(1) + gps_time = GPSTime.from_datetime(dt.datetime(utc_time.year, utc_time.month, utc_time.day)) + return download_nav(gps_time, cache_dir, ConstellationId.GPS) + + +def get_random_coords(lat, lon) -> Tuple[int, int]: + # jump around the world + # max values, lat: -90 to 90, lon: -180 to 180 + + lat_add = random.random()*20 + 10 + lon_add = random.random()*20 + 20 + + lat = ((lat + lat_add + 90) % 180) - 90 + lon = ((lon + lon_add + 180) % 360) - 180 + return round(lat, 5), round(lon, 5) + + +def check_availability() -> bool: + cmd = ["LimeSuite/builddir/LimeUtil/LimeUtil", "--find"] + output = sp.check_output(cmd) + + if output.strip() == b"": + return False + + print(f"Device: {output.strip().decode('utf-8')}") + return True + + +def main(): + if not os.path.exists('LimeGPS'): + print("LimeGPS not found run 'setup.sh' first") + return + + if not os.path.exists('LimeSuite'): + print("LimeSuite not found run 'setup.sh' first") + return + + if not check_availability(): + print("No limeSDR device found!") + return + + rinex_file = download_rinex() + lat, lon = get_random_coords(47.2020, 15.7403) + + if len(sys.argv) == 3: + lat = float(sys.argv[1]) + lon = float(sys.argv[2]) + + try: + print(f"starting LimeGPS, Location: {lat},{lon}") + cmd = ["LimeGPS/LimeGPS", "-e", rinex_file, "-l", f"{lat},{lon},100"] + sp.check_output(cmd, stderr=sp.PIPE) + except KeyboardInterrupt: + print("stopping LimeGPS") + except Exception as e: + out_stderr = e.stderr.decode('utf-8')# pylint:disable=no-member + if "Device is busy." in out_stderr: + print("GPS simulation is already running, Device is busy!") + return + + print(f"LimeGPS crashed: {str(e)}") + print(f"stderr:\n{e.stderr.decode('utf-8')}")# pylint:disable=no-member + +if __name__ == "__main__": + main() diff --git a/tools/gpstest/setup.sh b/tools/gpstest/setup.sh new file mode 100755 index 0000000000..ddf41dd260 --- /dev/null +++ b/tools/gpstest/setup.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +cd $DIR + +if [ ! -d LimeSuite ]; then + git clone https://github.com/myriadrf/LimeSuite.git + cd LimeSuite + # checkout latest version which has firmware updates available + git checkout v20.10.0 + git apply ../patches/limeSuite/* + mkdir builddir && cd builddir + cmake -DCMAKE_BUILD_TYPE=Release .. + make -j4 + cd ../.. +fi + +if [ ! -d LimeGPS ]; then + git clone https://github.com/osqzss/LimeGPS.git + cd LimeGPS + git apply ../patches/limeGPS/* + make + cd .. +fi diff --git a/tools/gpstest/test_gps.py b/tools/gpstest/test_gps.py new file mode 100644 index 0000000000..f5e19372f7 --- /dev/null +++ b/tools/gpstest/test_gps.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +import time +import unittest +import struct +import numpy as np + +import cereal.messaging as messaging +import selfdrive.sensord.pigeond as pd +from system.hardware import TICI +from selfdrive.test.helpers import with_processes + + +def read_events(service, duration_sec): + service_sock = messaging.sub_sock(service, timeout=0.1) + start_time_sec = time.monotonic() + events = [] + while time.monotonic() - start_time_sec < duration_sec: + events += messaging.drain_sock(service_sock) + time.sleep(0.1) + + assert len(events) != 0, f"No '{service}'events collected!" + return events + + +def verify_ubloxgnss_data(socket: messaging.SubSocket): + start_time = 0 + end_time = 0 + events = messaging.drain_sock(socket) + assert len(events) != 0, "no ublxGnss measurements" + + for event in events: + if event.ubloxGnss.which() != "measurementReport": + continue + + if start_time == 0: + start_time = event.logMonoTime + + if event.ubloxGnss.measurementReport.numMeas != 0: + end_time = event.logMonoTime + break + + assert end_time != 0, "no ublox measurements received!" + + ttfm = (end_time - start_time)/1e9 + assert ttfm < 35, f"Time to first measurement > 35s, {ttfm}" + + # check for satellite count in measurements + sat_count = [] + end_id = events.index(event)# pylint:disable=undefined-loop-variable + for event in events[end_id:]: + if event.ubloxGnss.which() == "measurementReport": + sat_count.append(event.ubloxGnss.measurementReport.numMeas) + + num_sat = int(sum(sat_count)/len(sat_count)) + assert num_sat > 8, f"Not enough satellites {num_sat} (TestBox setup!)" + + +def verify_gps_location(socket: messaging.SubSocket): + buf_lon = [0]*10 + buf_lat = [0]*10 + buf_i = 0 + events = messaging.drain_sock(socket) + assert len(events) != 0, "no gpsLocationExternal measurements" + + start_time = events[0].logMonoTime + end_time = 0 + for event in events: + buf_lon[buf_i % 10] = event.gpsLocationExternal.longitude + buf_lat[buf_i % 10] = event.gpsLocationExternal.latitude + buf_i += 1 + + if buf_i < 9: + continue + + if any([lat == 0 or lon == 0 for lat,lon in zip(buf_lat, buf_lon)]): + continue + + if np.std(buf_lon) < 1e-5 and np.std(buf_lat) < 1e-5: + end_time = event.logMonoTime + break + + assert end_time != 0, "GPS location never converged!" + + ttfl = (end_time - start_time)/1e9 + assert ttfl < 40, f"Time to first location > 40s, {ttfl}" + + hacc = events[-1].gpsLocationExternal.accuracy + vacc = events[-1].gpsLocationExternal.verticalAccuracy + assert hacc < 15, f"Horizontal accuracy too high, {hacc}" + assert vacc < 43, f"Vertical accuracy too high, {vacc}" + + +def verify_time_to_first_fix(pigeon): + # get time to first fix from nav status message + nav_status = b"" + while True: + pigeon.send(b"\xb5\x62\x01\x03\x00\x00\x04\x0d") + nav_status = pigeon.receive() + if nav_status[:4] == b"\xb5\x62\x01\x03": + break + + values = struct.unpack(" 40s, {ttff}" + + +class TestGPS(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not TICI: + raise unittest.SkipTest + + def tearDown(self): + pd.set_power(False) + + @with_processes(['ubloxd']) + def test_ublox_reset(self): + + pigeon, pm = pd.create_pigeon() + pd.init_baudrate(pigeon) + assert pigeon.reset_device(), "Could not reset device!" + + pd.initialize_pigeon(pigeon) + + ugs = messaging.sub_sock("ubloxGnss", timeout=0.1) + gle = messaging.sub_sock("gpsLocationExternal", timeout=0.1) + + # receive some messages (restart after cold start takes up to 30seconds) + pd.run_receiving(pigeon, pm, 40) + + verify_ubloxgnss_data(ugs) + verify_gps_location(gle) + + # skip for now, this might hang for a while + #verify_time_to_first_fix(pigeon) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tools/joystick/joystickd.py b/tools/joystick/joystickd.py index 8374cf8a74..b31dab83fe 100755 --- a/tools/joystick/joystickd.py +++ b/tools/joystick/joystickd.py @@ -38,13 +38,20 @@ class Keyboard: class Joystick: - def __init__(self): + def __init__(self, gamepad=False): # TODO: find a way to get this from API, perhaps "inputs" doesn't support it - self.min_axis_value = {'ABS_Y': 0., 'ABS_RZ': 0.} - self.max_axis_value = {'ABS_Y': 255., 'ABS_RZ': 255.} - self.cancel_button = 'BTN_TRIGGER' - self.axes_values = {'ABS_Y': 0., 'ABS_RZ': 0.} # gb, steer - self.axes_order = ['ABS_Y', 'ABS_RZ'] + if gamepad: + self.cancel_button = 'BTN_NORTH' # (BTN_NORTH=X, ABS_RZ=Right Trigger) + accel_axis = 'ABS_Y' + steer_axis = 'ABS_RX' + else: + self.cancel_button = 'BTN_TRIGGER' + accel_axis = 'ABS_Y' + steer_axis = 'ABS_RZ' + self.min_axis_value = {accel_axis: 0., steer_axis: 0.} + self.max_axis_value = {accel_axis: 255., steer_axis: 255.} + self.axes_values = {accel_axis: 0., steer_axis: 0.} + self.axes_order = [accel_axis, steer_axis] self.cancel = False def update(self): @@ -80,9 +87,8 @@ def send_thread(joystick): requests.get("http://"+os.environ["WEB"]+":5000/control/%f/%f" % tuple([joystick.axes_values[a] for a in joystick.axes_order][::-1]), timeout=None) rk.keep_time() -def joystick_thread(use_keyboard): +def joystick_thread(joystick): Params().put_bool('JoystickDebugMode', True) - joystick = Keyboard() if use_keyboard else Joystick() threading.Thread(target=send_thread, args=(joystick,), daemon=True).start() while True: joystick.update() @@ -92,6 +98,7 @@ if __name__ == '__main__': 'openpilot must be offroad before starting joysticked.', formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('--keyboard', action='store_true', help='Use your keyboard instead of a joystick') + parser.add_argument('--gamepad', action='store_true', help='Use gamepad configuration instead of joystick') args = parser.parse_args() if not Params().get_bool("IsOffroad") and "ZMQ" not in os.environ and "WEB" not in os.environ: @@ -108,4 +115,5 @@ if __name__ == '__main__': else: print('Using joystick, make sure to run cereal/messaging/bridge on your device if running over the network!') - joystick_thread(args.keyboard) + joystick = Keyboard() if args.keyboard else Joystick(args.gamepad) + joystick_thread(joystick) diff --git a/tools/lib/bootlog.py b/tools/lib/bootlog.py index 3515370823..1e474e5dde 100644 --- a/tools/lib/bootlog.py +++ b/tools/lib/bootlog.py @@ -1,6 +1,7 @@ import datetime import functools import re +from typing import List, Optional from tools.lib.auth_config import get_token from tools.lib.api import CommaApi @@ -48,8 +49,15 @@ class Bootlog: return False return self.datetime < b.datetime +def get_bootlog_from_id(bootlog_id: str) -> Optional[Bootlog]: + # TODO: implement an API endpoint for this + bl = Bootlog(bootlog_id) + for b in get_bootlogs(bl.dongle_id): + if b == bl: + return b + return None -def get_bootlogs(dongle_id: str): +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/replay/SConscript b/tools/replay/SConscript index d3967708fa..4ddeb662e0 100644 --- a/tools/replay/SConscript +++ b/tools/replay/SConscript @@ -12,14 +12,14 @@ else: base_libs.append('OpenCL') qt_libs = ['qt_util'] + base_libs -if arch in ['x86_64', 'Darwin'] or GetOption('extras'): - qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"] +qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"] - replay_lib_src = ["replay.cc", "consoleui.cc", "camera.cc", "filereader.cc", "logreader.cc", "framereader.cc", "route.cc", "util.cc"] +replay_lib_src = ["replay.cc", "consoleui.cc", "camera.cc", "filereader.cc", "logreader.cc", "framereader.cc", "route.cc", "util.cc"] - replay_lib = qt_env.Library("qt_replay", replay_lib_src, LIBS=qt_libs, FRAMEWORKS=base_frameworks) - replay_libs = [replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv', 'ncurses'] + qt_libs - qt_env.Program("replay", ["main.cc"], LIBS=replay_libs, FRAMEWORKS=base_frameworks) +replay_lib = qt_env.Library("qt_replay", replay_lib_src, LIBS=qt_libs, FRAMEWORKS=base_frameworks) +Export('replay_lib') +replay_libs = [replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv', 'ncurses'] + qt_libs +qt_env.Program("replay", ["main.cc"], LIBS=replay_libs, FRAMEWORKS=base_frameworks) - if GetOption('test'): - qt_env.Program('tests/test_replay', ['tests/test_runner.cc', 'tests/test_replay.cc'], LIBS=[replay_libs]) +if GetOption('test'): + qt_env.Program('tests/test_replay', ['tests/test_runner.cc', 'tests/test_replay.cc'], LIBS=[replay_libs]) diff --git a/tools/replay/logreader.cc b/tools/replay/logreader.cc index 9b7a07a83f..e3d5071412 100644 --- a/tools/replay/logreader.cc +++ b/tools/replay/logreader.cc @@ -46,7 +46,9 @@ LogReader::~LogReader() { #endif } -bool LogReader::load(const std::string &url, std::atomic *abort, bool local_cache, int chunk_size, int retries) { +bool LogReader::load(const std::string &url, std::atomic *abort, + const std::set &allow, + bool local_cache, int chunk_size, int retries) { raw_ = FileReader(local_cache, chunk_size, retries).read(url, abort); if (raw_.empty()) return false; @@ -54,18 +56,26 @@ bool LogReader::load(const std::string &url, std::atomic *abort, bool loca raw_ = decompressBZ2(raw_, abort); if (raw_.empty()) return false; } - return parse(abort); + return parse(allow, abort); } bool LogReader::load(const std::byte *data, size_t size, std::atomic *abort) { raw_.assign((const char *)data, size); - return parse(abort); + return parse({}, abort); } -bool LogReader::parse(std::atomic *abort) { +bool LogReader::parse(const std::set &allow, std::atomic *abort) { try { kj::ArrayPtr words((const capnp::word *)raw_.data(), raw_.size() / sizeof(capnp::word)); while (words.size() > 0 && !(abort && *abort)) { + if (!allow.empty()) { + capnp::FlatArrayMessageReader reader(words); + auto which = reader.getRoot().which(); + if (allow.find(which) == allow.end()) { + words = kj::arrayPtr(reader.getEnd(), words.end()); + continue; + } + } #ifdef HAS_MEMORY_RESOURCE Event *evt = new (mbr_) Event(words); diff --git a/tools/replay/logreader.h b/tools/replay/logreader.h index bd666d0a74..010839af22 100644 --- a/tools/replay/logreader.h +++ b/tools/replay/logreader.h @@ -5,6 +5,8 @@ #include #endif +#include + #include "cereal/gen/cpp/log.capnp.h" #include "system/camerad/cameras/camera_common.h" #include "tools/replay/filereader.h" @@ -50,12 +52,13 @@ class LogReader { public: LogReader(size_t memory_pool_block_size = DEFAULT_EVENT_MEMORY_POOL_BLOCK_SIZE); ~LogReader(); - bool load(const std::string &url, std::atomic *abort = nullptr, bool local_cache = false, int chunk_size = -1, int retries = 0); + bool load(const std::string &url, std::atomic *abort = nullptr, const std::set &allow = {}, + bool local_cache = false, int chunk_size = -1, int retries = 0); bool load(const std::byte *data, size_t size, std::atomic *abort = nullptr); std::vector events; private: - bool parse(std::atomic *abort); + bool parse(const std::set &allow, std::atomic *abort); std::string raw_; #ifdef HAS_MEMORY_RESOURCE std::pmr::monotonic_buffer_resource *mbr_ = nullptr; diff --git a/tools/replay/main.cc b/tools/replay/main.cc index d3d6894877..40dace0c91 100644 --- a/tools/replay/main.cc +++ b/tools/replay/main.cc @@ -4,8 +4,6 @@ #include "tools/replay/consoleui.h" #include "tools/replay/replay.h" -const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36"; - int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index 3e482b5474..1337a4ef2c 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -19,6 +19,9 @@ Replay::Replay(QString route, QStringList allow, QStringList block, SubMaster *s if ((allow.empty() || allow.contains(it.name)) && !block.contains(it.name)) { uint16_t which = event_struct.getFieldByName(it.name).getProto().getDiscriminantValue(); sockets_[which] = it.name; + if (!allow.empty() || !block.empty()) { + allow_list.insert((cereal::Event::Which)which); + } s.push_back(it.name); } } @@ -91,17 +94,17 @@ void Replay::updateEvents(const std::function &lambda) { stream_cv_.notify_one(); } -void Replay::seekTo(int seconds, bool relative) { +void Replay::seekTo(double seconds, bool relative) { seconds = relative ? seconds + currentSeconds() : seconds; updateEvents([&]() { - seconds = std::max(0, seconds); - int seg = seconds / 60; + seconds = std::max(double(0.0), seconds); + int seg = (int)seconds / 60; if (segments_.find(seg) == segments_.end()) { rWarning("can't seek to %d s segment %d is invalid", seconds, seg); return true; } - rInfo("seeking to %d s, segment %d", seconds, seg); + rInfo("seeking to %d s, segment %d", (int)seconds, seg); current_segment_ = seg; cur_mono_time_ = route_start_ts_ + seconds * 1e9; return isSegmentMerged(seg); @@ -122,7 +125,9 @@ void Replay::buildTimeline() { for (int i = 0; i < segments_.size() && !exit_; ++i) { LogReader log; - if (!log.load(route_->at(i).qlog.toStdString(), &exit_, !hasFlag(REPLAY_FLAG_NO_FILE_CACHE), 0, 3)) continue; + if (!log.load(route_->at(i).qlog.toStdString(), &exit_, + {cereal::Event::Which::CONTROLS_STATE, cereal::Event::Which::USER_FLAG}, + !hasFlag(REPLAY_FLAG_NO_FILE_CACHE), 0, 3)) continue; for (const Event *e : log.events) { if (e->which == cereal::Event::Which::CONTROLS_STATE) { @@ -206,7 +211,7 @@ void Replay::queueSegment() { SegmentMap::iterator cur, end; cur = end = segments_.lower_bound(std::min(current_segment_.load(), segments_.rbegin()->first)); - for (int i = 0; end != segments_.end() && i <= FORWARD_SEGS; ++i) { + for (int i = 0; end != segments_.end() && i <= segment_cache_limit + FORWARD_FETCH_SEGS; ++i) { ++end; } // load one segment at a time @@ -215,7 +220,7 @@ void Replay::queueSegment() { if ((seg && !seg->isLoaded()) || !seg) { if (!seg) { rDebug("loading segment %d...", n); - seg = std::make_unique(n, route_->at(n), flags_); + seg = std::make_unique(n, route_->at(n), flags_, allow_list); QObject::connect(seg.get(), &Segment::loadFinished, this, &Replay::segmentLoadFinished); } break; @@ -245,7 +250,7 @@ void Replay::mergeSegments(const SegmentMap::iterator &begin, const SegmentMap:: // merge 3 segments in sequence. std::vector segments_need_merge; size_t new_events_size = 0; - for (auto it = begin; it != end && it->second && it->second->isLoaded() && segments_need_merge.size() < 3; ++it) { + for (auto it = begin; it != end && it->second && it->second->isLoaded() && segments_need_merge.size() < segment_cache_limit; ++it) { segments_need_merge.push_back(it->first); new_events_size += it->second->log->events.size(); } @@ -270,6 +275,9 @@ void Replay::mergeSegments(const SegmentMap::iterator &begin, const SegmentMap:: segments_merged_ = segments_need_merge; return true; }); + if (stream_thread_) { + emit segmentsMerged(); + } } } @@ -290,6 +298,7 @@ void Replay::startStream(const Segment *cur_segment) { auto words = capnp::messageToFlatArray(builder); auto bytes = words.asBytes(); Params().put("CarParams", (const char *)bytes.begin(), bytes.size()); + Params().put("CarParamsPersistent", (const char *)bytes.begin(), bytes.size()); } else { rWarning("failed to read CarParams from current segment"); } @@ -305,6 +314,7 @@ void Replay::startStream(const Segment *cur_segment) { camera_server_ = std::make_unique(camera_size); } + emit segmentsMerged(); // start stream thread stream_thread_ = new QThread(); QObject::connect(stream_thread_, &QThread::started, [=]() { stream(); }); @@ -315,6 +325,8 @@ void Replay::startStream(const Segment *cur_segment) { } void Replay::publishMessage(const Event *e) { + if (event_filter && event_filter(e, filter_opaque)) return; + if (sm == nullptr) { auto bytes = e->bytes(); int ret = pm->send(sockets_[e->which], (capnp::byte *)bytes.begin(), bytes.size()); @@ -381,7 +393,7 @@ void Replay::stream() { if (cur_which < sockets_.size() && sockets_[cur_which] != nullptr) { // keep time - long etime = cur_mono_time_ - evt_start_ts; + long etime = (cur_mono_time_ - evt_start_ts) / speed_; long rtime = nanos_since_boot() - loop_start_ts; long behind_ns = etime - rtime; // if behind_ns is greater than 1 second, it means that an invalid segemnt is skipped by seeking/replaying diff --git a/tools/replay/replay.h b/tools/replay/replay.h index e86c453f7e..88c285125a 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -7,8 +7,10 @@ #include "tools/replay/camera.h" #include "tools/replay/route.h" +const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36"; + // one segment uses about 100M of memory -constexpr int FORWARD_SEGS = 5; +constexpr int FORWARD_FETCH_SEGS = 3; enum REPLAY_FLAGS { REPLAY_FLAG_NONE = 0x0000, @@ -32,6 +34,7 @@ enum class FindFlag { }; enum class TimelineType { None, Engaged, AlertInfo, AlertWarning, AlertCritical, UserFlag }; +typedef bool (*replayEventFilter)(const Event *, void *); class Replay : public QObject { Q_OBJECT @@ -45,15 +48,28 @@ public: void stop(); void pause(bool pause); void seekToFlag(FindFlag flag); - void seekTo(int seconds, bool relative); + void seekTo(double seconds, bool relative); inline bool isPaused() const { return paused_; } + // the filter is called in streaming thread.try to return quickly from it to avoid blocking streaming. + // the filter function must return true if the event should be filtered. + // otherwise it must return false. + inline void installEventFilter(replayEventFilter filter, void *opaque) { + filter_opaque = opaque; + event_filter = filter; + } + inline int segmentCacheLimit() const { return segment_cache_limit; } + inline void setSegmentCacheLimit(int n) { segment_cache_limit = std::max(3, n); } inline bool hasFlag(REPLAY_FLAGS flag) const { return flags_ & flag; } inline void addFlag(REPLAY_FLAGS flag) { flags_ |= flag; } inline void removeFlag(REPLAY_FLAGS flag) { flags_ &= ~flag; } inline const Route* route() const { return route_.get(); } - inline int currentSeconds() const { return (cur_mono_time_ - route_start_ts_) / 1e9; } + inline double currentSeconds() const { return double(cur_mono_time_ - route_start_ts_) / 1e9; } + inline uint64_t routeStartTime() const { return route_start_ts_; } inline int toSeconds(uint64_t mono_time) const { return (mono_time - route_start_ts_) / 1e9; } inline int totalSeconds() const { return segments_.size() * 60; } + inline void setSpeed(float speed) { speed_ = speed; } + inline float getSpeed() const { return speed_; } + inline const std::vector *events() const { return events_.get(); } inline const std::string &carFingerprint() const { return car_fingerprint_; } inline const std::vector> getTimeline() { std::lock_guard lk(timeline_lock); @@ -62,6 +78,7 @@ public: signals: void streamStarted(); + void segmentsMerged(); protected slots: void segmentLoadFinished(bool success); @@ -95,7 +112,7 @@ protected: bool paused_ = false; bool events_updated_ = false; uint64_t route_start_ts_ = 0; - uint64_t cur_mono_time_ = 0; + std::atomic cur_mono_time_ = 0; std::unique_ptr> events_; std::unique_ptr> new_events_; std::vector segments_merged_; @@ -111,5 +128,10 @@ protected: std::mutex timeline_lock; QFuture timeline_future; std::vector> timeline; + std::set allow_list; std::string car_fingerprint_; + float speed_ = 1.0; + replayEventFilter event_filter = nullptr; + void *filter_opaque = nullptr; + int segment_cache_limit = 3; }; diff --git a/tools/replay/route.cc b/tools/replay/route.cc index c91b27ae81..f0d6ec5a12 100644 --- a/tools/replay/route.cc +++ b/tools/replay/route.cc @@ -99,7 +99,9 @@ void Route::addFileToSegment(int n, const QString &file) { // class Segment -Segment::Segment(int n, const SegmentFile &files, uint32_t flags) : seg_num(n), flags(flags) { +Segment::Segment(int n, const SegmentFile &files, uint32_t flags, + const std::set &allow) + : seg_num(n), flags(flags), allow(allow) { // [RoadCam, DriverCam, WideRoadCam, log]. fallback to qcamera/qlog const std::array file_list = { (flags & REPLAY_FLAG_QCAMERA) || files.road_cam.isEmpty() ? files.qcamera : files.road_cam, @@ -130,7 +132,7 @@ void Segment::loadFile(int id, const std::string file) { success = frames[id]->load(file, flags & REPLAY_FLAG_NO_HW_DECODER, &abort_, local_cache, 20 * 1024 * 1024, 3); } else { log = std::make_unique(); - success = log->load(file, &abort_, local_cache, 0, 3); + success = log->load(file, &abort_, allow, local_cache, 0, 3); } if (!success) { diff --git a/tools/replay/route.h b/tools/replay/route.h index 6ca9c3b883..6b78ebad87 100644 --- a/tools/replay/route.h +++ b/tools/replay/route.h @@ -47,7 +47,7 @@ class Segment : public QObject { Q_OBJECT public: - Segment(int n, const SegmentFile &files, uint32_t flags); + Segment(int n, const SegmentFile &files, uint32_t flags, const std::set &allow = {}); ~Segment(); inline bool isLoaded() const { return !loading_ && !abort_; } @@ -65,4 +65,5 @@ protected: std::atomic loading_ = 0; QFutureSynchronizer synchronizer_; uint32_t flags; + std::set allow; }; diff --git a/tools/replay/tests/test_replay.cc b/tools/replay/tests/test_replay.cc index d6482c3ca2..5b61b6b6f2 100644 --- a/tools/replay/tests/test_replay.cc +++ b/tools/replay/tests/test_replay.cc @@ -9,7 +9,6 @@ #include "tools/replay/replay.h" #include "tools/replay/util.h" -const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36"; const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; const std::string TEST_RLOG_CHECKSUM = "5b966d4bb21a100a8c4e59195faeb741b975ccbe268211765efd1763d892bfb3"; diff --git a/tools/sim/README.md b/tools/sim/README.md index cb1aea35ac..40603f3f71 100644 --- a/tools/sim/README.md +++ b/tools/sim/README.md @@ -39,6 +39,8 @@ Options: --high_quality Set simulator to higher quality (requires good GPU) --town TOWN Select map to drive in --spawn_point NUM Number of the spawn point to start in + --host HOST Host address of Carla client (127.0.0.1 as default) + --port PORT Port of Carla client (2000 as default) ``` To engage openpilot press 1 a few times while focused on bridge.py to increase the cruise speed. diff --git a/tools/sim/bridge.py b/tools/sim/bridge.py index 0e4f47963b..a105ed751e 100755 --- a/tools/sim/bridge.py +++ b/tools/sim/bridge.py @@ -29,7 +29,7 @@ REPEAT_COUNTER = 5 PRINT_DECIMATION = 100 STEER_RATIO = 15. -pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState', 'sensorEvents', 'can', "gpsLocationExternal"]) +pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState', 'accelerometer', 'gyroscope', 'can', "gpsLocationExternal"]) sm = messaging.SubMaster(['carControl', 'controlsState']) def parse_args(add_args=None): @@ -39,6 +39,8 @@ def parse_args(add_args=None): parser.add_argument('--dual_camera', action='store_true') parser.add_argument('--town', type=str, default='Town04_Opt') parser.add_argument('--spawn_point', dest='num_selected_spawn_point', type=int, default=16) + parser.add_argument('--host', dest='host', type=str, default='127.0.0.1') + parser.add_argument('--port', dest='port', type=int, default=2000) return parser.parse_args(add_args) @@ -122,18 +124,26 @@ class Camerad: pm.send(pub_type, dat) def imu_callback(imu, vehicle_state): - vehicle_state.bearing_deg = math.degrees(imu.compass) - dat = messaging.new_message('sensorEvents', 2) - dat.sensorEvents[0].sensor = 4 - dat.sensorEvents[0].type = 0x10 - dat.sensorEvents[0].init('acceleration') - dat.sensorEvents[0].acceleration.v = [imu.accelerometer.x, imu.accelerometer.y, imu.accelerometer.z] - # copied these numbers from locationd - dat.sensorEvents[1].sensor = 5 - dat.sensorEvents[1].type = 0x10 - dat.sensorEvents[1].init('gyroUncalibrated') - dat.sensorEvents[1].gyroUncalibrated.v = [imu.gyroscope.x, imu.gyroscope.y, imu.gyroscope.z] - pm.send('sensorEvents', dat) + # send 5x since 'sensor_tick' doesn't seem to work. limited by the world tick? + for _ in range(5): + vehicle_state.bearing_deg = math.degrees(imu.compass) + dat = messaging.new_message('accelerometer') + dat.accelerometer.sensor = 4 + dat.accelerometer.type = 0x1 + dat.accelerometer.timestamp = dat.logMonoTime # TODO: use the IMU timestamp + dat.accelerometer.init('acceleration') + dat.accelerometer.acceleration.v = [imu.accelerometer.x, imu.accelerometer.y, imu.accelerometer.z] + pm.send('accelerometer', dat) + + # copied these numbers from locationd + dat = messaging.new_message('gyroscope') + dat.gyroscope.sensor = 5 + dat.gyroscope.type = 0x10 + dat.gyroscope.timestamp = dat.logMonoTime # TODO: use the IMU timestamp + dat.gyroscope.init('gyroUncalibrated') + dat.gyroscope.gyroUncalibrated.v = [imu.gyroscope.x, imu.gyroscope.y, imu.gyroscope.z] + pm.send('gyroscope', dat) + time.sleep(0.01) def panda_state_function(vs: VehicleState, exit_event: threading.Event): @@ -225,8 +235,8 @@ def can_function_runner(vs: VehicleState, exit_event: threading.Event): i += 1 -def connect_carla_client(): - client = carla.Client("127.0.0.1", 2000) +def connect_carla_client(host: str, port: int): + client = carla.Client(host, port) client.set_timeout(5) return client @@ -236,11 +246,13 @@ class CarlaBridge: def __init__(self, arguments): set_params_enabled() + self.params = Params() + msg = messaging.new_message('liveCalibration') msg.liveCalibration.validBlocks = 20 msg.liveCalibration.rpyCalib = [0.0, 0.0, 0.0] - Params().put("CalibrationParams", msg.to_bytes()) - Params().put_bool("WideCameraOnly", not arguments.dual_camera) + self.params.put("CalibrationParams", msg.to_bytes()) + self.params.put_bool("WideCameraOnly", not arguments.dual_camera) self._args = arguments self._carla_objects = [] @@ -281,7 +293,7 @@ class CarlaBridge: self.close() def _run(self, q: Queue): - client = connect_carla_client() + client = connect_carla_client(self._args.host, self._args.port) world = client.load_world(self._args.town) settings = world.get_settings() @@ -348,12 +360,14 @@ class CarlaBridge: # re-enable IMU imu_bp = blueprint_library.find('sensor.other.imu') + imu_bp.set_attribute('sensor_tick', '0.01') imu = world.spawn_actor(imu_bp, transform, attach_to=vehicle) imu.listen(lambda imu: imu_callback(imu, vehicle_state)) gps_bp = blueprint_library.find('sensor.other.gnss') gps = world.spawn_actor(gps_bp, transform, attach_to=vehicle) gps.listen(lambda gps: gps_callback(gps, vehicle_state)) + self.params.put_bool("UbloxAvailable", True) self._carla_objects.extend([imu, gps]) # launch fake car threads diff --git a/tools/sim/launch_openpilot.sh b/tools/sim/launch_openpilot.sh index 15f45b4cc2..adabc40c2e 100755 --- a/tools/sim/launch_openpilot.sh +++ b/tools/sim/launch_openpilot.sh @@ -3,6 +3,7 @@ export PASSIVE="0" export NOBOARD="1" export SIMULATION="1" +export SKIP_FW_QUERY="1" export FINGERPRINT="HONDA CIVIC 2016" export BLOCK="camerad,loggerd,encoderd" diff --git a/tools/ubuntu_setup.sh b/tools/ubuntu_setup.sh index 863b853718..7e021bcc23 100755 --- a/tools/ubuntu_setup.sh +++ b/tools/ubuntu_setup.sh @@ -63,6 +63,7 @@ function install_ubuntu_common_requirements() { qttools5-dev-tools \ libqt5sql5-sqlite \ libqt5svg5-dev \ + libqt5charts5-dev \ libqt5x11extras5-dev \ libreadline-dev \ libdw1 \