diff --git a/.github/labeler.yaml b/.github/labeler.yaml index e91764bb37..278ac58036 100644 --- a/.github/labeler.yaml +++ b/.github/labeler.yaml @@ -1,6 +1,6 @@ ci: - changed-files: - - any-glob-to-all-files: "{.github/**,**/test_*,Jenkinsfile}" + - any-glob-to-all-files: "{.github/**,**/test_*,**/test/**,Jenkinsfile}" chore: - changed-files: diff --git a/.github/workflows/auto_pr_review.yaml b/.github/workflows/auto_pr_review.yaml index ec78f276a5..b9664b9066 100644 --- a/.github/workflows/auto_pr_review.yaml +++ b/.github/workflows/auto_pr_review.yaml @@ -1,7 +1,7 @@ name: "PR review" on: pull_request_target: - types: [opened, reopened, synchronize, edited] + types: [ opened, reopened, synchronize, edited ] jobs: labeler: @@ -39,7 +39,7 @@ jobs: name: Update fork's PR Labels runs-on: ubuntu-latest if: (github.event.pull_request.head.repo.fork && (contains(github.event_name, 'pull_request') && github.event.action == 'synchronize')) - env: + env: PR_LABEL: 'dev-c3' TRUST_FORK_PR_LABEL: 'trust-fork-pr' steps: @@ -64,7 +64,7 @@ jobs: core.setOutput('has-dev-c3', hasDevC3Label ? 'true' : 'false'); core.setOutput('has-trust', hasTrustLabel ? 'true' : 'false'); - + - name: Remove trust-fork-pr label if present if: steps.check-labels.outputs.has-dev-c3 == 'true' && steps.check-labels.outputs.has-trust == 'true' uses: actions/github-script@v7 @@ -72,14 +72,14 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const prNumber = context.payload.pull_request.number; - + await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, name: process.env.TRUST_FORK_PR_LABEL }); - + console.log(`Removed '${process.env.TRUST_FORK_PR_LABEL}' label from PR #${prNumber} as it received new commits`); // Add a comment to the PR diff --git a/.github/workflows/badges.yaml b/.github/workflows/badges.yaml index 505148beac..3019ba9cba 100644 --- a/.github/workflows/badges.yaml +++ b/.github/workflows/badges.yaml @@ -5,8 +5,8 @@ on: workflow_dispatch: env: - BASE_IMAGE: openpilot-base - DOCKER_REGISTRY: ghcr.io/commaai + BASE_IMAGE: sunnypilot-base + DOCKER_REGISTRY: ghcr.io/sunnypilot RUN: docker run --shm-size 2G -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 $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $DOCKER_REGISTRY/$BASE_IMAGE:latest /bin/bash -c jobs: diff --git a/.github/workflows/build-all-tinygrad-models.yaml b/.github/workflows/build-all-tinygrad-models.yaml index dbc9c52823..8b86c6b043 100644 --- a/.github/workflows/build-all-tinygrad-models.yaml +++ b/.github/workflows/build-all-tinygrad-models.yaml @@ -16,7 +16,24 @@ jobs: recompiled_dir: ${{ steps.create-recompiled-dir.outputs.recompiled_dir }} json_file: ${{ steps.get-json.outputs.json_file }} model_matrix: ${{ steps.set-matrix.outputs.model_matrix }} + tinygrad_ref: ${{ steps.get-tinygrad-ref.outputs.tinygrad_ref }} steps: + - name: Checkout sunnypilot repo + uses: actions/checkout@v4 + with: + repository: sunnypilot/sunnypilot + path: sunnypilot + submodules: recursive + + - name: Get tinygrad_repo ref + id: get-tinygrad-ref + run: | + cd sunnypilot + export PYTHONPATH=$(pwd) + ref=$(python3 sunnypilot/models/tinygrad_ref.py) + echo "tinygrad_ref=$ref" >> $GITHUB_OUTPUT + echo "tinygrad_ref is $ref" + - name: Checkout docs repo (sunnypilot-docs, gh-pages) uses: actions/checkout@v4 with: @@ -269,6 +286,7 @@ jobs: ARGS="" [ -n "${{ inputs.set_min_version }}" ] && ARGS="$ARGS --set-min-version \"${{ inputs.set_min_version }}\"" ARGS="$ARGS --sort-by-date" + ARGS="$ARGS --tinygrad-ref \"${{ needs.setup.outputs.tinygrad_ref }}\"" eval python3 docs/json_parser.py \ --json-path "$JSON_FILE" \ --recompiled-dir "gitlab_docs/models/$RECOMPILED_DIR" \ diff --git a/.github/workflows/build-single-tinygrad-model.yaml b/.github/workflows/build-single-tinygrad-model.yaml index 53ce2458f0..76f39fb6fe 100644 --- a/.github/workflows/build-single-tinygrad-model.yaml +++ b/.github/workflows/build-single-tinygrad-model.yaml @@ -42,11 +42,11 @@ on: recompiled_dir: description: 'Existing recompiled directory number (e.g. 3 for recompiled3)' required: true - type: number + type: string json_version: description: 'driving_models version number to update (e.g. 5 for driving_models_v5.json)' required: true - type: number + type: string model_folder: description: 'Model folder' type: choice @@ -67,11 +67,11 @@ on: generation: description: 'Model generation' required: false - type: number + type: string version: description: 'Minimum selector version' required: false - type: number + type: string env: RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }} JSON_FILE: docs/docs/driving_models_v${{ inputs.json_version }}.json diff --git a/.github/workflows/jenkins-pr-trigger.yaml b/.github/workflows/jenkins-pr-trigger.yaml index db1d524018..14e2fdf49b 100644 --- a/.github/workflows/jenkins-pr-trigger.yaml +++ b/.github/workflows/jenkins-pr-trigger.yaml @@ -9,6 +9,9 @@ jobs: scan-comments: runs-on: ubuntu-latest if: ${{ github.event.issue.pull_request }} + permissions: + contents: write + issues: write steps: - name: Check for trigger phrase id: check_comment @@ -43,3 +46,14 @@ jobs: git config --global user.email "github-actions[bot]@users.noreply.github.com" git checkout -b tmp-jenkins-${{ github.event.issue.number }} GIT_LFS_SKIP_PUSH=1 git push -f origin tmp-jenkins-${{ github.event.issue.number }} + + - name: Delete trigger comment + if: steps.check_comment.outputs.result == 'true' && always() + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + }); diff --git a/.github/workflows/lfs-maintenance.yaml b/.github/workflows/lfs-maintenance.yaml index aea918b530..8780abfbb3 100644 --- a/.github/workflows/lfs-maintenance.yaml +++ b/.github/workflows/lfs-maintenance.yaml @@ -13,7 +13,7 @@ on: pull_request: branches: - 'master' - workflow_dispatch: # enables manual triggering + workflow_dispatch: # enables manual triggering inputs: upstream_branch: default: 'master' @@ -30,7 +30,7 @@ jobs: with: repository: 'commaai/openpilot' ref: ${{ inputs.upstream_branch }} - + - name: LFS Fetch run: | git lfs fetch @@ -48,7 +48,7 @@ jobs: - name: Add GitLab public keys run: | ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts - + - name: Ensure branch run: | if git symbolic-ref -q HEAD >/dev/null; then diff --git a/.github/workflows/prebuilt.yaml b/.github/workflows/prebuilt.yaml index 61e3f3bc4f..dde290d5c5 100644 --- a/.github/workflows/prebuilt.yaml +++ b/.github/workflows/prebuilt.yaml @@ -6,7 +6,7 @@ on: env: DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} - BUILD: selfdrive/test/docker_build.sh prebuilt + BUILD: release/ci/docker_build_sp.sh prebuilt jobs: build_prebuilt: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d2228a2079..3b8ddd408f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -5,12 +5,12 @@ on: workflow_dispatch: jobs: - build_masterci: - name: build master-ci + build___nightly: + name: build __nightly env: ImageOS: ubuntu24 container: - image: ghcr.io/commaai/openpilot-base:latest + image: ghcr.io/sunnypilot/sunnypilot-base:latest runs-on: ubuntu-latest if: github.repository == 'sunnypilot/sunnypilot' permissions: @@ -27,7 +27,7 @@ jobs: with: ref: master wait-interval: 30 - running-workflow-name: 'build master-ci' + running-workflow-name: 'build __nightly' repo-token: ${{ secrets.GITHUB_TOKEN }} check-regexp: ^((?!.*(build prebuilt).*).)*$ - uses: actions/checkout@v4 @@ -38,5 +38,5 @@ jobs: run: | git config --global --add safe.directory '*' git lfs pull - - name: Push master-ci + - name: Push __nightly run: BRANCH=__nightly release/build_devel.sh diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index 2b73c0a418..72de377086 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -6,8 +6,8 @@ on: workflow_dispatch: env: - BASE_IMAGE: openpilot-base - BUILD: selfdrive/test/docker_build.sh base + BASE_IMAGE: sunnypilot-base + BUILD: release/ci/docker_build_sp.sh base RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c jobs: @@ -36,7 +36,7 @@ jobs: name: package_updates runs-on: ubuntu-latest container: - image: ghcr.io/commaai/openpilot-base:latest + image: ghcr.io/sunnypilot/sunnypilot-base:latest if: github.repository == 'sunnypilot/sunnypilot' steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index b031f2f634..912ac787c3 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -18,13 +18,12 @@ concurrency: cancel-in-progress: true env: - REPORT_NAME: report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} PYTHONWARNINGS: error - BASE_IMAGE: openpilot-base + BASE_IMAGE: sunnypilot-base AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }} DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} - BUILD: selfdrive/test/docker_build.sh base + BUILD: release/ci/docker_build_sp.sh base RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c @@ -32,10 +31,13 @@ env: jobs: build_release: - if: github.repository == 'commaai/openpilot' # build_release blocked for the time being to only comma as we may have a different process. name: build release - runs-on: - - 'ubuntu-24.04' + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} env: STRIPPED_DIR: /tmp/releasepilot steps: @@ -65,17 +67,37 @@ jobs: - name: Check submodules if: github.repository == 'sunnypilot/sunnypilot' timeout-minutes: 3 - run: release/check-submodules.sh + run: | + if [ "${{ github.ref }}" != "refs/heads/master" ]; then + git fetch origin master:refs/remotes/origin/master + + SUBMODULE_PATHS=$(git diff origin/master HEAD --name-only | grep -E '^[^/]+$' | while read path; do + if git ls-files --stage "$path" | grep -q "^160000"; then + echo "$path" + fi + done | tr '\n' ' ') + + if [ -n "$SUBMODULE_PATHS" ]; then + echo "Changed submodule paths: $SUBMODULE_PATHS" + export SUBMODULE_PATHS="$SUBMODULE_PATHS" + export CHECK_PR_REFS=true + fi + fi + release/check-submodules.sh build: - runs-on: - - 'ubuntu-24.04' + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} steps: - uses: actions/checkout@v4 with: submodules: true - name: Setup docker push - if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot' + if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'sunnypilot/sunnypilot' run: | echo "PUSH_IMAGE=true" >> "$GITHUB_ENV" $DOCKER_LOGIN @@ -85,6 +107,7 @@ jobs: build_mac: name: build macOS + if: false # temp disable since homebrew install is getting stuck runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }} steps: - uses: actions/checkout@v4 @@ -130,8 +153,12 @@ jobs: static_analysis: name: static analysis - runs-on: - - 'ubuntu-latest' + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} env: PYTHONWARNINGS: default steps: @@ -145,19 +172,25 @@ jobs: unit_tests: name: unit tests - runs-on: - - 'ubuntu-24.04' + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} steps: - uses: actions/checkout@v4 with: submodules: true - uses: ./.github/workflows/setup-with-retry + id: setup-step - name: Build openpilot run: ${{ env.RUN }} "scons -j$(nproc)" - name: Run unit tests - timeout-minutes: ${{ contains(runner.name, 'nsc') && 1 || 20 }} + timeout-minutes: ${{ contains(runner.name, 'nsc') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 999 }} run: | - ${{ env.RUN }} "$PYTEST --collect-only -m 'not slow' &> /dev/null && \ + ${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \ + $PYTEST --collect-only -m 'not slow' &> /dev/null && \ MAX_EXAMPLES=1 $PYTEST -m 'not slow' && \ ./selfdrive/ui/tests/create_test_translations.sh && \ QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \ @@ -165,14 +198,19 @@ jobs: process_replay: name: process replay - if: github.repository == 'commaai/openpilot' # disable process_replay for forks - runs-on: - - 'ubuntu-24.04' + if: false # disable process_replay for forks + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} steps: - uses: actions/checkout@v4 with: submodules: true - uses: ./.github/workflows/setup-with-retry + id: setup-step - name: Cache test routes id: dependency-cache uses: actions/cache@v4 @@ -183,7 +221,7 @@ jobs: run: | ${{ env.RUN }} "scons -j$(nproc)" - name: Run replay - timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && 1 || 20 }} + timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }} run: | ${{ env.RUN }} "selfdrive/test/process_replay/test_processes.py -j$(nproc) && \ chmod -R 777 /tmp/comma_download_cache" @@ -208,106 +246,26 @@ jobs: ${{ env.RUN }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \ chmod -R 777 /tmp/comma_download_cache" - test_cars: - name: cars - runs-on: - - 'ubuntu-24.04' - strategy: - fail-fast: false - matrix: - job: [0, 1, 2, 3] - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - uses: ./.github/workflows/setup-with-retry - - name: Cache test routes - id: routes-cache - uses: actions/cache@v4 - with: - path: .ci_cache/comma_download_cache - key: car_models-${{ hashFiles('selfdrive/car/tests/test_models.py', 'opendbc/car/tests/routes.py') }}-${{ matrix.job }} - - name: Build openpilot - run: ${{ env.RUN }} "scons -j$(nproc)" - - name: Test car models - timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.routes-cache.outputs.cache-hit == 'true') && 1 || 6 }} - run: | - ${{ env.RUN }} "MAX_EXAMPLES=1 $PYTEST selfdrive/car/tests/test_models.py && \ - chmod -R 777 /tmp/comma_download_cache" - env: - NUM_JOBS: 4 - JOB_ID: ${{ matrix.job }} - - car_docs_diff: - name: PR comments - runs-on: ubuntu-latest - #if: github.event_name == 'pull_request' - if: false # TODO: run this in opendbc? - steps: - - uses: actions/checkout@v4 - with: - submodules: true - ref: ${{ github.event.pull_request.base.ref }} - - run: git lfs pull - - uses: ./.github/workflows/setup-with-retry - - name: Get base car info - run: | - ${{ env.RUN }} "scons -j$(nproc) && python3 selfdrive/debug/dump_car_docs.py --path /tmp/openpilot_cache/base_car_docs" - sudo chown -R $USER:$USER ${{ github.workspace }} - - uses: actions/checkout@v4 - with: - submodules: true - path: current - - run: cd current && git lfs pull - - name: Save car docs diff - id: save_diff - run: | - cd current - ${{ env.RUN }} "scons -j$(nproc)" - output=$(${{ env.RUN }} "python3 selfdrive/debug/print_docs_diff.py --path /tmp/openpilot_cache/base_car_docs") - output="${output//$'\n'/'%0A'}" - echo "::set-output name=diff::$output" - - name: Find comment - if: ${{ env.AZURE_TOKEN != '' }} - uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e - id: fc - with: - issue-number: ${{ github.event.pull_request.number }} - body-includes: This PR makes changes to - - name: Update comment - if: ${{ steps.save_diff.outputs.diff != '' && env.AZURE_TOKEN != '' }} - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 - with: - comment-id: ${{ steps.fc.outputs.comment-id }} - issue-number: ${{ github.event.pull_request.number }} - body: "${{ steps.save_diff.outputs.diff }}" - edit-mode: replace - - name: Delete comment - if: ${{ steps.fc.outputs.comment-id != '' && steps.save_diff.outputs.diff == '' && env.AZURE_TOKEN != '' }} - uses: actions/github-script@v7 - with: - script: | - github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: ${{ steps.fc.outputs.comment-id }} - }) - simulator_driving: name: simulator driving - runs-on: - - 'ubuntu-24.04' + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} if: (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) steps: - uses: actions/checkout@v4 with: submodules: true - uses: ./.github/workflows/setup-with-retry + id: setup-step - name: Build openpilot run: | ${{ env.RUN }} "scons -j$(nproc)" - name: Driving test - timeout-minutes: 1 + timeout-minutes: ${{ (steps.setup-step.outputs.duration < 18) && 1 || 2 }} run: | ${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \ source selfdrive/test/setup_vsound.sh && \ @@ -316,8 +274,13 @@ jobs: create_ui_report: # This job name needs to be the same as UI_JOB_NAME in ui_preview.yaml name: Create UI Report - runs-on: - - 'ubuntu-24.04' + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} + if: false # FIXME: FrameReader is broken on CI runners steps: - uses: actions/checkout@v4 with: @@ -341,5 +304,5 @@ jobs: - name: Upload Test Report uses: actions/upload-artifact@v4 with: - name: ${{ env.REPORT_NAME }} + name: report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} path: selfdrive/ui/tests/test_ui/report_1/screenshots diff --git a/.github/workflows/setup-with-retry/action.yaml b/.github/workflows/setup-with-retry/action.yaml index ad297403cf..98a3913600 100644 --- a/.github/workflows/setup-with-retry/action.yaml +++ b/.github/workflows/setup-with-retry/action.yaml @@ -10,9 +10,17 @@ inputs: required: false default: 30 +outputs: + duration: + description: 'Duration of the setup process in seconds' + value: ${{ steps.get_duration.outputs.duration }} + runs: using: "composite" steps: + - id: start_time + shell: bash + run: echo "START_TIME=$(date +%s)" >> $GITHUB_ENV - id: setup1 uses: ./.github/workflows/setup continue-on-error: true @@ -35,3 +43,10 @@ runs: uses: ./.github/workflows/setup with: is_retried: true + - id: get_duration + shell: bash + run: | + END_TIME=$(date +%s) + DURATION=$((END_TIME - START_TIME)) + echo "Total duration: $DURATION seconds" + echo "duration=$DURATION" >> $GITHUB_OUTPUT diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index cd785b3f56..4f52e7fa29 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -80,10 +80,10 @@ jobs: - name: Set Feature Branch Prebuilt Configuration id: set_feature_configuration if: ( - !(env.SOURCE_BRANCH == env.DEV_C3_SOURCE_BRANCH) && - !(env.SOURCE_BRANCH == env.STAGING_C3_SOURCE_BRANCH) && - !(startsWith(github.ref, 'refs/tags/')) - ) + !(env.SOURCE_BRANCH == env.DEV_C3_SOURCE_BRANCH) && + !(env.SOURCE_BRANCH == env.STAGING_C3_SOURCE_BRANCH) && + !(startsWith(github.ref, 'refs/tags/')) + ) run: | echo "NEW_BRANCH=${{ env.SOURCE_BRANCH }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}-prebuilt" >> $GITHUB_ENV echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV @@ -91,9 +91,9 @@ jobs: - name: Set dev-c3-new prebuilt Configuration id: set_dev_configuration if: ( - steps.set_feature_configuration.outcome == 'skipped' && - env.SOURCE_BRANCH == env.DEV_C3_SOURCE_BRANCH - ) + steps.set_feature_configuration.outcome == 'skipped' && + env.SOURCE_BRANCH == env.DEV_C3_SOURCE_BRANCH + ) run: | echo "NEW_BRANCH=${{ env.DEV_TARGET_BRANCH }}" >> $GITHUB_ENV echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV @@ -102,11 +102,11 @@ jobs: - name: Set staging-c3-new prebuilt Configuration id: set_staging_configuration if: ( - steps.set_feature_configuration.outcome == 'skipped' && - !contains(github.event_name, 'pull_request') && - steps.set_dev_configuration.outcome == 'skipped' && - (env.SOURCE_BRANCH == env.STAGING_C3_SOURCE_BRANCH) - ) + steps.set_feature_configuration.outcome == 'skipped' && + !contains(github.event_name, 'pull_request') && + steps.set_dev_configuration.outcome == 'skipped' && + (env.SOURCE_BRANCH == env.STAGING_C3_SOURCE_BRANCH) + ) run: | echo "NEW_BRANCH=${{ env.STAGING_TARGET_BRANCH }}" >> $GITHUB_ENV echo "EXTRA_VERSION_IDENTIFIER=staging" >> $GITHUB_ENV @@ -115,11 +115,11 @@ jobs: - name: Set release-c3-new prebuilt Configuration id: set_tag_configuration if: ( - steps.set_feature_configuration.outcome == 'skipped' && - !contains(github.event_name, 'pull_request') && - steps.set_staging_configuration.outcome == 'skipped' && - startsWith(github.ref, 'refs/tags/') - ) + steps.set_feature_configuration.outcome == 'skipped' && + !contains(github.event_name, 'pull_request') && + steps.set_staging_configuration.outcome == 'skipped' && + startsWith(github.ref, 'refs/tags/') + ) run: | echo "NEW_BRANCH=${{ env.RELEASE_TARGET_BRANCH }}" >> $GITHUB_ENV echo "EXTRA_VERSION_IDENTIFIER=release" >> $GITHUB_ENV @@ -227,7 +227,6 @@ jobs: run: | PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable - publish: concurrency: group: publish-${{ github.head_ref || github.ref_name }} @@ -268,7 +267,7 @@ jobs: "${{ needs.build.outputs.version }}" \ "https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \ "-${{ needs.build.outputs.extra_version_identifier }}" - + echo "" echo "---- ℹ️ To update the list of branches that auto deploy prebuilts -----" echo "" @@ -299,7 +298,7 @@ jobs: export extra_version_identifier=${{ needs.build.outputs.extra_version_identifier || github.run_number}} echo ${TEMPLATE} | envsubst | jq -c '.' | tee payload.json curl -X POST -H "Content-Type: application/json" -d @payload.json $DISCORD_WEBHOOK - + echo "" echo "---- ℹ️ To update the list of branches that notify to dev-feedback -----" echo "" @@ -307,7 +306,7 @@ jobs: echo "2. Current value: ${{ vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES }}" echo "3. Update as needed (JSON array with no spaces)" shell: alpine.sh {0} - + manage-pr-labels: name: Remove prebuilt label runs-on: ubuntu-latest diff --git a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml index 13c8d5cb37..d4c201824e 100644 --- a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml @@ -12,8 +12,8 @@ on: branches: - master pull_request_target: - types: [ labeled ] - branches: + types: [ labeled ] + branches: - 'master' workflow_dispatch: inputs: @@ -41,10 +41,10 @@ jobs: reset-and-squash: runs-on: ubuntu-latest if: ( - (github.event_name == 'workflow_dispatch') - || (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) - || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) - ) + (github.event_name == 'workflow_dispatch') + || (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) + || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) + ) steps: - uses: actions/checkout@v4 with: @@ -54,9 +54,9 @@ jobs: - name: Wait for Tests uses: ./.github/workflows/wait-for-action # Path to where you place the action if: ( - (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) - || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) - ) + (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) + || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) + ) with: workflow: selfdrive_tests.yaml # The workflow file to monitor github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index db569f06cb..3c0dad521d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,11 @@ venv/ model2.png a.out .hypothesis +.cache/ /docs_site/ +*.mp4 *.dylib *.DSYM *.d diff --git a/Dockerfile.sunnypilot b/Dockerfile.sunnypilot new file mode 100644 index 0000000000..88a226ad08 --- /dev/null +++ b/Dockerfile.sunnypilot @@ -0,0 +1,12 @@ +FROM ghcr.io/sunnypilot/sunnypilot-base:latest + +ENV PYTHONUNBUFFERED=1 + +ENV OPENPILOT_PATH=/home/batman/openpilot + +RUN mkdir -p ${OPENPILOT_PATH} +WORKDIR ${OPENPILOT_PATH} + +COPY . ${OPENPILOT_PATH}/ + +RUN scons --cache-readonly -j$(nproc) diff --git a/Dockerfile.sunnypilot_base b/Dockerfile.sunnypilot_base new file mode 100644 index 0000000000..ec1e3b6c06 --- /dev/null +++ b/Dockerfile.sunnypilot_base @@ -0,0 +1,83 @@ +FROM ubuntu:24.04 + +ENV PYTHONUNBUFFERED=1 + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && \ + apt-get install -y --no-install-recommends sudo tzdata locales ssh pulseaudio xvfb x11-xserver-utils gnome-screenshot python3-tk python3-dev && \ + rm -rf /var/lib/apt/lists/* + +RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en +ENV LC_ALL=en_US.UTF-8 + +COPY tools/install_ubuntu_dependencies.sh /tmp/tools/ +RUN /tmp/tools/install_ubuntu_dependencies.sh && \ + rm -rf /var/lib/apt/lists/* /tmp/* && \ + cd /usr/lib/gcc/arm-none-eabi/* && \ + rm -rf arm/ thumb/nofp thumb/v6* thumb/v8* thumb/v7+fp thumb/v7-r+fp.sp + +# Add OpenCL +RUN apt-get update && apt-get install -y --no-install-recommends \ + apt-utils \ + alien \ + unzip \ + tar \ + curl \ + xz-utils \ + dbus \ + gcc-arm-none-eabi \ + tmux \ + vim \ + libx11-6 \ + wget \ + rsync \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p /tmp/opencl-driver-intel && \ + cd /tmp/opencl-driver-intel && \ + wget https://github.com/intel/llvm/releases/download/2024-WW14/oclcpuexp-2024.17.3.0.09_rel.tar.gz && \ + wget https://github.com/oneapi-src/oneTBB/releases/download/v2021.12.0/oneapi-tbb-2021.12.0-lin.tgz && \ + mkdir -p /opt/intel/oclcpuexp_2024.17.3.0.09_rel && \ + cd /opt/intel/oclcpuexp_2024.17.3.0.09_rel && \ + tar -zxvf /tmp/opencl-driver-intel/oclcpuexp-2024.17.3.0.09_rel.tar.gz && \ + mkdir -p /etc/OpenCL/vendors && \ + echo /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64/libintelocl.so > /etc/OpenCL/vendors/intel_expcpu.icd && \ + cd /opt/intel && \ + tar -zxvf /tmp/opencl-driver-intel/oneapi-tbb-2021.12.0-lin.tgz && \ + ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbb.so /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \ + ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbbmalloc.so /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \ + ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbb.so.12 /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \ + ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbbmalloc.so.2 /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \ + mkdir -p /etc/ld.so.conf.d && \ + echo /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 > /etc/ld.so.conf.d/libintelopenclexp.conf && \ + ldconfig -f /etc/ld.so.conf.d/libintelopenclexp.conf && \ + cd / && \ + rm -rf /tmp/opencl-driver-intel + +ENV NVIDIA_VISIBLE_DEVICES=all +ENV NVIDIA_DRIVER_CAPABILITIES=graphics,utility,compute +ENV QTWEBENGINE_DISABLE_SANDBOX=1 + +RUN dbus-uuidgen > /etc/machine-id +RUN apt-get update && apt-get install -y fonts-noto-cjk fonts-noto-color-emoji + +ARG USER=batman +ARG USER_UID=1001 +RUN useradd -m -s /bin/bash -u $USER_UID $USER +RUN usermod -aG sudo $USER +RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers +USER $USER + +COPY --chown=$USER pyproject.toml uv.lock /home/$USER +COPY --chown=$USER tools/install_python_dependencies.sh /home/$USER/tools/ + +ENV VIRTUAL_ENV=/home/$USER/.venv +ENV PATH="$VIRTUAL_ENV/bin:$PATH" +RUN cd /home/$USER && \ + tools/install_python_dependencies.sh && \ + rm -rf tools/ pyproject.toml uv.lock .cache + +USER root +RUN sudo git config --global --add safe.directory /tmp/openpilot diff --git a/RELEASES.md b/RELEASES.md index 58d0d07a74..584be9d67e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,10 +1,14 @@ -Version 0.10.0 (2025-07-07) +Version 0.10.0 (2025-08-05) ======================== * New driving model - * Lead car ground-truth fixes - * Ported over VAE from the MLSIM stack - * New training architecture described in CVPR paper + * New training architecture + * Architecture outlined in CVPR paper: "Learning to Drive from a World Model" + * Longitudinal MPC replaced by E2E planning from worldmodel in experimental mode + * Action from lateral MPC as training objective replaced by E2E planning from worldmodel + * Low-speed lead car ground-truth fixes + * Enable live-learned steering actuation delay +* Record driving feedback using LKAS button when MADS is disabled * Opt-in audio recording for dashcam video Version 0.9.9 (2025-05-23) diff --git a/SConstruct b/SConstruct index 6bfe6c3025..192f83a0f6 100644 --- a/SConstruct +++ b/SConstruct @@ -17,7 +17,7 @@ AGNOS = TICI Decider('MD5-timestamp') -SetOption('num_jobs', int(os.cpu_count()/2)) +SetOption('num_jobs', max(1, int(os.cpu_count()/2))) AddOption('--kaitai', action='store_true', @@ -82,7 +82,6 @@ assert arch in ["larch64", "aarch64", "x86_64", "Darwin"] lenv = { "PATH": os.environ['PATH'], - "LD_LIBRARY_PATH": [Dir(f"#third_party/acados/{arch}/lib").abspath], "PYTHONPATH": Dir("#").abspath + ':' + Dir(f"#third_party/acados").abspath, "ACADOS_SOURCE_DIR": Dir("#third_party/acados").abspath, @@ -90,7 +89,7 @@ lenv = { "TERA_PATH": Dir("#").abspath + f"/third_party/acados/{arch}/t_renderer" } -rpath = lenv["LD_LIBRARY_PATH"].copy() +rpath = [] if arch == "larch64": cpppath = [ @@ -133,7 +132,6 @@ else: f"{brew_prefix}/include", f"{brew_prefix}/opt/openssl@3.0/include", ] - lenv["DYLD_LIBRARY_PATH"] = lenv["LD_LIBRARY_PATH"] # Linux else: libpath = [ diff --git a/cereal/log.capnp b/cereal/log.capnp index 65dc243dab..15795f8c38 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -127,7 +127,9 @@ struct OnroadEvent @0xc4fa6047f024e718 { espActive @90; personalityChanged @91; aeb @92; - userFlag @95; + userBookmark @95; + excessiveActuation @96; + audioFeedback @97; soundsUnavailableDEPRECATED @47; } @@ -492,7 +494,6 @@ struct DeviceState @0xa4d8b5af2aa492eb { gpuTempC @27 :List(Float32); dspTempC @49 :Float32; memoryTempC @28 :Float32; - nvmeTempC @35 :List(Float32); modemTempC @36 :List(Float32); pmicTempC @39 :List(Float32); intakeTempC @46 :Float32; @@ -568,6 +569,7 @@ struct DeviceState @0xa4d8b5af2aa492eb { chargingDisabledDEPRECATED @18 :Bool; usbOnlineDEPRECATED @12 :Bool; ambientTempCDEPRECATED @30 :Float32; + nvmeTempCDEPRECATED @35 :List(Float32); } struct PandaState @0xa7649e2575e4591e { @@ -585,7 +587,7 @@ struct PandaState @0xa7649e2575e4591e { fanPower @28 :UInt8; fanStallCount @34 :UInt8; - spiChecksumErrorCount @33 :UInt16; + spiErrorCount @33 :UInt16; harnessStatus @21 :HarnessStatus; sbu1Voltage @35 :Float32; @@ -2467,7 +2469,7 @@ struct DebugAlert { alertText2 @1 :Text; } -struct UserFlag { +struct UserBookmark @0xfe346a9de48d9b50 { } struct SoundPressure @0xdc24138990726023 { @@ -2485,6 +2487,11 @@ struct AudioData { sampleRate @1 :UInt32; } +struct AudioFeedback { + audio @0 :AudioData; + blockNum @1 :UInt16; +} + struct Touch { sec @0 :Int64; usec @1 :Int64; @@ -2585,9 +2592,13 @@ struct Event { mapRenderState @105: MapRenderState; # UI services - userFlag @93 :UserFlag; uiDebug @102 :UIDebug; + # driving feedback + userBookmark @93 :UserBookmark; + bookmarkButton @148 :UserBookmark; + audioFeedback @149 :AudioFeedback; + # *********** debug *********** testJoystick @52 :Joystick; roadEncodeData @86 :EncodeData; diff --git a/cereal/messaging/tests/test_messaging.py b/cereal/messaging/tests/test_messaging.py index 6234a11c08..583eb8b0d8 100644 --- a/cereal/messaging/tests/test_messaging.py +++ b/cereal/messaging/tests/test_messaging.py @@ -30,7 +30,7 @@ def zmq_sleep(t=1): # TODO: this should take any capnp struct and returrn a msg with random populated data def random_carstate(): - fields = ["vEgo", "aEgo", "gas", "steeringAngleDeg"] + fields = ["vEgo", "aEgo", "brake", "steeringAngleDeg"] msg = messaging.new_message("carState") cs = msg.carState for f in fields: @@ -177,8 +177,8 @@ class TestMessaging: # wait 5 socket timeouts before sending msg = random_carstate() - delayed_send(sock_timeout*5, pub_sock, msg.to_bytes()) start_time = time.monotonic() + delayed_send(sock_timeout*5, pub_sock, msg.to_bytes()) recvd = messaging.recv_one_retry(sub_sock) assert (time.monotonic() - start_time) >= sock_timeout*5 assert isinstance(recvd, capnp._DynamicStructReader) diff --git a/cereal/messaging/tests/test_pub_sub_master.py b/cereal/messaging/tests/test_pub_sub_master.py index e47e713393..5e26b49701 100644 --- a/cereal/messaging/tests/test_pub_sub_master.py +++ b/cereal/messaging/tests/test_pub_sub_master.py @@ -86,7 +86,7 @@ class TestSubMaster: "cameraOdometry": (20, 10), "liveCalibration": (4, 4), "carParams": (None, None), - "userFlag": (None, None), + "userBookmark": (None, None), } for service, (max_freq, min_freq) in checks.items(): diff --git a/cereal/services.py b/cereal/services.py index 4418d079a3..373e865e34 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -72,9 +72,11 @@ _services: dict[str, tuple] = { "navRoute": (True, 0.), "navThumbnail": (True, 0.), "qRoadEncodeIdx": (False, 20.), - "userFlag": (True, 0., 1), + "userBookmark": (True, 0., 1), "soundPressure": (True, 10., 10), "rawAudioData": (False, 20.), + "bookmarkButton": (True, 0., 1), + "audioFeedback": (True, 0., 1), # sunnypilot "modelManagerSP": (False, 1., 1), diff --git a/common/conversions.py b/common/constants.py similarity index 83% rename from common/conversions.py rename to common/constants.py index b02b33c625..7ca425c4b2 100644 --- a/common/conversions.py +++ b/common/constants.py @@ -1,6 +1,7 @@ import numpy as np -class Conversions: +# conversions +class CV: # Speed MPH_TO_KPH = 1.609344 KPH_TO_MPH = 1. / MPH_TO_KPH @@ -17,3 +18,6 @@ class Conversions: # Mass LB_TO_KG = 0.453592 + + +ACCELERATION_DUE_TO_GRAVITY = 9.81 # m/s^2 diff --git a/common/model.h b/common/model.h index ce5482e9e1..90e8096716 100644 --- a/common/model.h +++ b/common/model.h @@ -1 +1 @@ -#define DEFAULT_MODEL "Tomb Raider 14 (Default)" +#define DEFAULT_MODEL "Space Lab 2 (Default)" diff --git a/common/params.cc b/common/params.cc index 0a8821c37a..39592cb905 100644 --- a/common/params.cc +++ b/common/params.cc @@ -103,10 +103,10 @@ Params::~Params() { assert(queue.empty()); } -std::vector Params::allKeys(ParamKeyType type) const { +std::vector Params::allKeys(ParamKeyFlag flag) const { std::vector ret; for (auto &p : keys) { - if (type == ALL || (p.second & type)) { + if (flag == ALL || (p.second.flags & flag)) { ret.push_back(p.first); } } @@ -117,8 +117,16 @@ bool Params::checkKey(const std::string &key) { return keys.find(key) != keys.end(); } +ParamKeyFlag Params::getKeyFlag(const std::string &key) { + return static_cast(keys[key].flags); +} + ParamKeyType Params::getKeyType(const std::string &key) { - return static_cast(keys[key]); + return keys[key].type; +} + +std::optional Params::getKeyDefaultValue(const std::string &key) { + return keys[key].default_value; } int Params::put(const char* key, const char* value, size_t value_size) { @@ -197,17 +205,17 @@ std::map Params::readAll() { return util::read_files_in_dir(getParamPath()); } -void Params::clearAll(ParamKeyType key_type) { +void Params::clearAll(ParamKeyFlag key_flag) { FileLock file_lock(params_path + "/.lock"); - // 1) delete params of key_type + // 1) delete params of key_flag // 2) delete files that are not defined in the keys. if (DIR *d = opendir(getParamPath().c_str())) { struct dirent *de = NULL; while ((de = readdir(d))) { if (de->d_type != DT_DIR) { auto it = keys.find(de->d_name); - if (it == keys.end() || (it->second & key_type)) { + if (it == keys.end() || (it->second.flags & key_flag)) { unlink(getParamPath(de->d_name).c_str()); } } diff --git a/common/params.h b/common/params.h index 73e21a3fee..de4f9b435f 100644 --- a/common/params.h +++ b/common/params.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -9,17 +10,34 @@ #include "common/queue.h" -enum ParamKeyType { +enum ParamKeyFlag { PERSISTENT = 0x02, CLEAR_ON_MANAGER_START = 0x04, CLEAR_ON_ONROAD_TRANSITION = 0x08, CLEAR_ON_OFFROAD_TRANSITION = 0x10, DONT_LOG = 0x20, DEVELOPMENT_ONLY = 0x40, - BACKUP = 0x80, + CLEAR_ON_IGNITION_ON = 0x80, + BACKUP = 0x100, ALL = 0xFFFFFFFF }; +enum ParamKeyType { + STRING = 0, // must be utf-8 decodable + BOOL = 1, + INT = 2, + FLOAT = 3, + TIME = 4, // ISO 8601 + JSON = 5, + BYTES = 6 +}; + +struct ParamKeyAttributes { + uint32_t flags; + ParamKeyType type; + std::optional default_value = std::nullopt; +}; + class Params { public: explicit Params(const std::string &path = {}); @@ -28,16 +46,18 @@ public: Params(const Params&) = delete; Params& operator=(const Params&) = delete; - std::vector allKeys(ParamKeyType type = ALL) const; + std::vector allKeys(ParamKeyFlag flag = ALL) const; bool checkKey(const std::string &key); + ParamKeyFlag getKeyFlag(const std::string &key); ParamKeyType getKeyType(const std::string &key); + std::optional getKeyDefaultValue(const std::string &key); inline std::string getParamPath(const std::string &key = {}) { return params_path + params_prefix + (key.empty() ? "" : "/" + key); } // Delete a value int remove(const std::string &key); - void clearAll(ParamKeyType type); + void clearAll(ParamKeyFlag flag); // helpers for reading values std::string get(const std::string &key, bool block = false); diff --git a/common/params.py b/common/params.py index 66808083dc..494617200f 100644 --- a/common/params.py +++ b/common/params.py @@ -1,5 +1,6 @@ -from openpilot.common.params_pyx import Params, ParamKeyType, UnknownKeyName +from openpilot.common.params_pyx import Params, ParamKeyFlag, ParamKeyType, UnknownKeyName assert Params +assert ParamKeyFlag assert ParamKeyType assert UnknownKeyName diff --git a/common/params_keys.h b/common/params_keys.h index dfcf410a51..08f79bf5af 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -3,208 +3,216 @@ #include #include -inline static std::unordered_map keys = { - {"AccessToken", CLEAR_ON_MANAGER_START | DONT_LOG}, - {"AdbEnabled", PERSISTENT | BACKUP}, - {"AlwaysOnDM", PERSISTENT | BACKUP}, - {"ApiCache_Device", PERSISTENT}, - {"ApiCache_FirehoseStats", PERSISTENT}, - {"AssistNowToken", PERSISTENT}, - {"AthenadPid", PERSISTENT}, - {"AthenadUploadQueue", PERSISTENT}, - {"AthenadRecentlyViewedRoutes", PERSISTENT}, - {"BootCount", PERSISTENT}, - {"CalibrationParams", PERSISTENT}, - {"CameraDebugExpGain", CLEAR_ON_MANAGER_START}, - {"CameraDebugExpTime", CLEAR_ON_MANAGER_START}, - {"CarBatteryCapacity", PERSISTENT}, - {"CarParams", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"CarParamsCache", CLEAR_ON_MANAGER_START}, - {"CarParamsPersistent", PERSISTENT}, - {"CarParamsPrevRoute", PERSISTENT}, - {"CompletedTrainingVersion", PERSISTENT}, - {"ControlsReady", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"CurrentBootlog", PERSISTENT}, - {"CurrentRoute", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"DisableLogging", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"DisablePowerDown", PERSISTENT | BACKUP}, - {"DisableUpdates", PERSISTENT | BACKUP}, - {"DisengageOnAccelerator", PERSISTENT | BACKUP}, - {"DongleId", PERSISTENT}, - {"DoReboot", CLEAR_ON_MANAGER_START}, - {"DoShutdown", CLEAR_ON_MANAGER_START}, - {"DoUninstall", CLEAR_ON_MANAGER_START}, - {"AlphaLongitudinalEnabled", PERSISTENT | DEVELOPMENT_ONLY | BACKUP}, - {"ExperimentalMode", PERSISTENT | BACKUP}, - {"ExperimentalModeConfirmed", PERSISTENT | BACKUP}, - {"FirmwareQueryDone", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"ForcePowerDown", PERSISTENT}, - {"GitBranch", PERSISTENT}, - {"GitCommit", PERSISTENT}, - {"GitCommitDate", PERSISTENT}, - {"GitDiff", PERSISTENT}, - {"GithubSshKeys", PERSISTENT | BACKUP}, - {"GithubUsername", PERSISTENT | BACKUP}, - {"GitRemote", PERSISTENT}, - {"GsmApn", PERSISTENT | BACKUP}, - {"GsmMetered", PERSISTENT | BACKUP}, - {"GsmRoaming", PERSISTENT | BACKUP}, - {"HardwareSerial", PERSISTENT}, - {"HasAcceptedTerms", PERSISTENT}, - {"InstallDate", PERSISTENT}, - {"IsDriverViewEnabled", CLEAR_ON_MANAGER_START}, - {"IsEngaged", PERSISTENT}, - {"IsLdwEnabled", PERSISTENT | BACKUP}, - {"IsMetric", PERSISTENT | BACKUP}, - {"IsOffroad", CLEAR_ON_MANAGER_START}, - {"IsOnroad", PERSISTENT}, - {"IsRhdDetected", PERSISTENT}, - {"IsReleaseBranch", CLEAR_ON_MANAGER_START}, - {"IsTakingSnapshot", CLEAR_ON_MANAGER_START}, - {"IsTestedBranch", CLEAR_ON_MANAGER_START}, - {"JoystickDebugMode", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"LanguageSetting", PERSISTENT | BACKUP}, - {"LastAthenaPingTime", CLEAR_ON_MANAGER_START}, - {"LastGPSPosition", PERSISTENT}, - {"LastManagerExitReason", CLEAR_ON_MANAGER_START}, - {"LastOffroadStatusPacket", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"LastPowerDropDetected", CLEAR_ON_MANAGER_START}, - {"LastUpdateException", CLEAR_ON_MANAGER_START}, - {"LastUpdateTime", PERSISTENT}, - {"LiveDelay", PERSISTENT | BACKUP}, - {"LiveParameters", PERSISTENT}, - {"LiveParametersV2", PERSISTENT}, - {"LiveTorqueParameters", PERSISTENT | DONT_LOG}, - {"LocationFilterInitialState", PERSISTENT}, - {"LongitudinalManeuverMode", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"LongitudinalPersonality", PERSISTENT | BACKUP}, - {"NetworkMetered", PERSISTENT | BACKUP}, - {"ObdMultiplexingChanged", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"ObdMultiplexingEnabled", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"Offroad_BadNvme", CLEAR_ON_MANAGER_START}, - {"Offroad_CarUnrecognized", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"Offroad_ConnectivityNeeded", CLEAR_ON_MANAGER_START}, - {"Offroad_ConnectivityNeededPrompt", CLEAR_ON_MANAGER_START}, - {"Offroad_IsTakingSnapshot", CLEAR_ON_MANAGER_START}, - {"Offroad_NeosUpdate", CLEAR_ON_MANAGER_START}, - {"Offroad_NoFirmware", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"Offroad_Recalibration", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"Offroad_StorageMissing", CLEAR_ON_MANAGER_START}, - {"Offroad_TemperatureTooHigh", CLEAR_ON_MANAGER_START}, - {"Offroad_UnofficialHardware", CLEAR_ON_MANAGER_START}, - {"Offroad_UpdateFailed", CLEAR_ON_MANAGER_START}, - {"OnroadCycleRequested", CLEAR_ON_MANAGER_START}, - {"OpenpilotEnabledToggle", PERSISTENT | BACKUP}, - {"PandaHeartbeatLost", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"PandaSomResetTriggered", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"PandaSignatures", CLEAR_ON_MANAGER_START}, - {"PrimeType", PERSISTENT}, - {"RecordAudio", PERSISTENT | BACKUP}, - {"RecordFront", PERSISTENT | BACKUP}, - {"RecordFrontLock", PERSISTENT}, // for the internal fleet - {"SecOCKey", PERSISTENT | DONT_LOG | BACKUP}, - {"RouteCount", PERSISTENT}, - {"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"SshEnabled", PERSISTENT | BACKUP}, - {"TermsVersion", PERSISTENT}, - {"TrainingVersion", PERSISTENT}, - {"UbloxAvailable", PERSISTENT}, - {"UpdateAvailable", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"UpdateFailedCount", CLEAR_ON_MANAGER_START}, - {"UpdaterAvailableBranches", PERSISTENT}, - {"UpdaterCurrentDescription", CLEAR_ON_MANAGER_START}, - {"UpdaterCurrentReleaseNotes", CLEAR_ON_MANAGER_START}, - {"UpdaterFetchAvailable", CLEAR_ON_MANAGER_START}, - {"UpdaterNewDescription", CLEAR_ON_MANAGER_START}, - {"UpdaterNewReleaseNotes", CLEAR_ON_MANAGER_START}, - {"UpdaterState", CLEAR_ON_MANAGER_START}, - {"UpdaterTargetBranch", CLEAR_ON_MANAGER_START}, - {"UpdaterLastFetchTime", PERSISTENT}, - {"Version", PERSISTENT}, +#include "cereal/gen/cpp/log.capnp.h" + +inline static std::unordered_map keys = { + {"AccessToken", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}}, + {"AdbEnabled", {PERSISTENT | BACKUP, BOOL}}, + {"AlwaysOnDM", {PERSISTENT | BACKUP, BOOL}}, + {"ApiCache_Device", {PERSISTENT, STRING}}, + {"ApiCache_FirehoseStats", {PERSISTENT, JSON}}, + {"AssistNowToken", {PERSISTENT, STRING}}, + {"AthenadPid", {PERSISTENT, INT}}, + {"AthenadUploadQueue", {PERSISTENT, JSON}}, + {"AthenadRecentlyViewedRoutes", {PERSISTENT, STRING}}, + {"BootCount", {PERSISTENT, INT}}, + {"CalibrationParams", {PERSISTENT, BYTES}}, + {"CameraDebugExpGain", {CLEAR_ON_MANAGER_START, STRING}}, + {"CameraDebugExpTime", {CLEAR_ON_MANAGER_START, STRING}}, + {"CarBatteryCapacity", {PERSISTENT, INT}}, + {"CarParams", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BYTES}}, + {"CarParamsCache", {CLEAR_ON_MANAGER_START, BYTES}}, + {"CarParamsPersistent", {PERSISTENT, BYTES}}, + {"CarParamsPrevRoute", {PERSISTENT, BYTES}}, + {"CompletedTrainingVersion", {PERSISTENT, STRING, "0"}}, + {"ControlsReady", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, + {"CurrentBootlog", {PERSISTENT, STRING}}, + {"CurrentRoute", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}}, + {"DisableLogging", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, + {"DisablePowerDown", {PERSISTENT | BACKUP, BOOL}}, + {"DisableUpdates", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"DisengageOnAccelerator", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"DongleId", {PERSISTENT, STRING}}, + {"DoReboot", {CLEAR_ON_MANAGER_START, BOOL}}, + {"DoShutdown", {CLEAR_ON_MANAGER_START, BOOL}}, + {"DoUninstall", {CLEAR_ON_MANAGER_START, BOOL}}, + {"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}}, + {"AlphaLongitudinalEnabled", {PERSISTENT | DEVELOPMENT_ONLY | BACKUP, BOOL}}, + {"ExperimentalMode", {PERSISTENT | BACKUP, BOOL}}, + {"ExperimentalModeConfirmed", {PERSISTENT | BACKUP, BOOL}}, + {"FirmwareQueryDone", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, + {"ForcePowerDown", {PERSISTENT, BOOL}}, + {"GitBranch", {PERSISTENT, STRING}}, + {"GitCommit", {PERSISTENT, STRING}}, + {"GitCommitDate", {PERSISTENT, STRING}}, + {"GitDiff", {PERSISTENT, STRING}}, + {"GithubSshKeys", {PERSISTENT | BACKUP, STRING}}, + {"GithubUsername", {PERSISTENT | BACKUP, STRING}}, + {"GitRemote", {PERSISTENT, STRING}}, + {"GsmApn", {PERSISTENT | BACKUP, STRING}}, + {"GsmMetered", {PERSISTENT | BACKUP, BOOL, "1"}}, + {"GsmRoaming", {PERSISTENT | BACKUP, BOOL}}, + {"HardwareSerial", {PERSISTENT, STRING}}, + {"HasAcceptedTerms", {PERSISTENT, STRING, "0"}}, + {"InstallDate", {PERSISTENT, TIME}}, + {"IsDriverViewEnabled", {CLEAR_ON_MANAGER_START, BOOL}}, + {"IsEngaged", {PERSISTENT, BOOL}}, + {"IsLdwEnabled", {PERSISTENT | BACKUP, BOOL}}, + {"IsMetric", {PERSISTENT | BACKUP, BOOL}}, + {"IsOffroad", {CLEAR_ON_MANAGER_START, BOOL}}, + {"IsOnroad", {PERSISTENT, BOOL}}, + {"IsRhdDetected", {PERSISTENT, BOOL}}, + {"IsReleaseBranch", {CLEAR_ON_MANAGER_START, BOOL}}, + {"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}}, + {"IsTestedBranch", {CLEAR_ON_MANAGER_START, BOOL}}, + {"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, + {"LanguageSetting", {PERSISTENT | BACKUP, STRING, "main_en"}}, + {"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}}, + {"LastGPSPosition", {PERSISTENT, STRING}}, + {"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}}, + {"LastOffroadStatusPacket", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON}}, + {"LastPowerDropDetected", {CLEAR_ON_MANAGER_START, STRING}}, + {"LastUpdateException", {CLEAR_ON_MANAGER_START, STRING}}, + {"LastUpdateRouteCount", {PERSISTENT, INT}}, + {"LastUpdateTime", {PERSISTENT, TIME}}, + {"LastUpdateUptimeOnroad", {PERSISTENT, FLOAT}}, + {"LiveDelay", {PERSISTENT | BACKUP, BYTES}}, + {"LiveParameters", {PERSISTENT, JSON}}, + {"LiveParametersV2", {PERSISTENT, BYTES}}, + {"LiveTorqueParameters", {PERSISTENT | DONT_LOG, BYTES}}, + {"LocationFilterInitialState", {PERSISTENT, BYTES}}, + {"LongitudinalManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, + {"LongitudinalPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast(cereal::LongitudinalPersonality::STANDARD))}}, + {"NetworkMetered", {PERSISTENT | BACKUP, BOOL}}, + {"ObdMultiplexingChanged", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, + {"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, + {"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_ExcessiveActuation", {PERSISTENT, JSON}}, + {"Offroad_IsTakingSnapshot", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_NeosUpdate", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_NoFirmware", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_Recalibration", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_StorageMissing", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_TemperatureTooHigh", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_UnregisteredHardware", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_UpdateFailed", {CLEAR_ON_MANAGER_START, JSON}}, + {"OnroadCycleRequested", {CLEAR_ON_MANAGER_START, BOOL}}, + {"OpenpilotEnabledToggle", {PERSISTENT | BACKUP, BOOL, "1"}}, + {"PandaHeartbeatLost", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, + {"PandaSomResetTriggered", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, + {"PandaSignatures", {CLEAR_ON_MANAGER_START, BYTES}}, + {"PrimeType", {PERSISTENT, INT}}, + {"RecordAudio", {PERSISTENT | BACKUP, BOOL}}, + {"RecordAudioFeedback", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"RecordFront", {PERSISTENT | BACKUP, BOOL}}, + {"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet + {"SecOCKey", {PERSISTENT | DONT_LOG | BACKUP, STRING}}, + {"RouteCount", {PERSISTENT, INT, "0"}}, + {"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, + {"SshEnabled", {PERSISTENT | BACKUP, BOOL}}, + {"TermsVersion", {PERSISTENT, STRING}}, + {"TrainingVersion", {PERSISTENT, STRING}}, + {"UbloxAvailable", {PERSISTENT, BOOL}}, + {"UpdateAvailable", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, + {"UpdateFailedCount", {CLEAR_ON_MANAGER_START, INT}}, + {"UpdaterAvailableBranches", {PERSISTENT, STRING}}, + {"UpdaterCurrentDescription", {CLEAR_ON_MANAGER_START, STRING}}, + {"UpdaterCurrentReleaseNotes", {CLEAR_ON_MANAGER_START, BYTES}}, + {"UpdaterFetchAvailable", {CLEAR_ON_MANAGER_START, BOOL}}, + {"UpdaterNewDescription", {CLEAR_ON_MANAGER_START, STRING}}, + {"UpdaterNewReleaseNotes", {CLEAR_ON_MANAGER_START, BYTES}}, + {"UpdaterState", {CLEAR_ON_MANAGER_START, STRING}}, + {"UpdaterTargetBranch", {CLEAR_ON_MANAGER_START, STRING}}, + {"UpdaterLastFetchTime", {PERSISTENT, TIME}}, + {"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}}, + {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, + {"Version", {PERSISTENT, STRING}}, // --- sunnypilot params --- // - {"ApiCache_DriveStats", PERSISTENT}, - {"AutoLaneChangeBsmDelay", PERSISTENT | BACKUP}, - {"AutoLaneChangeTimer", PERSISTENT | BACKUP}, - {"BlinkerMinLateralControlSpeed", PERSISTENT | BACKUP}, - {"BlinkerPauseLateralControl", PERSISTENT | BACKUP}, - {"Brightness", PERSISTENT | BACKUP}, - {"CarParamsSP", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"CarParamsSPCache", CLEAR_ON_MANAGER_START}, - {"CarParamsSPPersistent", PERSISTENT}, - {"CarPlatformBundle", PERSISTENT | BACKUP}, - {"ChevronInfo", PERSISTENT | BACKUP}, - {"CustomAccIncrementsEnabled", PERSISTENT | BACKUP}, - {"CustomAccLongPressIncrement", PERSISTENT | BACKUP}, - {"CustomAccShortPressIncrement", PERSISTENT | BACKUP}, - {"DeviceBootMode", PERSISTENT | BACKUP}, - {"EnableGithubRunner", PERSISTENT | BACKUP}, - {"InteractivityTimeout", PERSISTENT | BACKUP}, - {"IsDevelopmentBranch", CLEAR_ON_MANAGER_START}, - {"MaxTimeOffroad", PERSISTENT | BACKUP}, - {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, - {"OffroadMode", CLEAR_ON_MANAGER_START}, - {"QuickBootToggle", PERSISTENT | BACKUP}, - {"QuietMode", PERSISTENT | BACKUP}, - {"ShowAdvancedControls", PERSISTENT | BACKUP}, + {"ApiCache_DriveStats", {PERSISTENT, JSON}}, + {"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}}, + {"BlinkerMinLateralControlSpeed", {PERSISTENT | BACKUP, INT, "20"}}, // MPH or km/h + {"BlinkerPauseLateralControl", {PERSISTENT | BACKUP, INT, "0"}}, + {"Brightness", {PERSISTENT | BACKUP, INT, "0"}}, + {"CarParamsSP", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BYTES}}, + {"CarParamsSPCache", {CLEAR_ON_MANAGER_START, BYTES}}, + {"CarParamsSPPersistent", {PERSISTENT, BYTES}}, + {"CarPlatformBundle", {PERSISTENT | BACKUP, JSON}}, + {"ChevronInfo", {PERSISTENT | BACKUP, INT, "4"}}, + {"CustomAccIncrementsEnabled", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"CustomAccLongPressIncrement", {PERSISTENT | BACKUP, INT, "5"}}, + {"CustomAccShortPressIncrement", {PERSISTENT | BACKUP, INT, "1"}}, + {"DeviceBootMode", {PERSISTENT | BACKUP, INT, "0"}}, + {"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}}, + {"InteractivityTimeout", {PERSISTENT | BACKUP, INT, "0"}}, + {"IsDevelopmentBranch", {CLEAR_ON_MANAGER_START, BOOL}}, + {"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}}, + {"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}}, + {"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}}, + {"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}}, // MADS params - {"Mads", PERSISTENT | BACKUP}, - {"MadsMainCruiseAllowed", PERSISTENT | BACKUP}, - {"MadsSteeringMode", PERSISTENT | BACKUP}, - {"MadsUnifiedEngagementMode", PERSISTENT | BACKUP}, + {"Mads", {PERSISTENT | BACKUP, BOOL, "1"}}, + {"MadsMainCruiseAllowed", {PERSISTENT | BACKUP, BOOL, "1"}}, + {"MadsSteeringMode", {PERSISTENT | BACKUP, INT, "0"}}, + {"MadsUnifiedEngagementMode", {PERSISTENT | BACKUP, BOOL, "1"}}, // Model Manager params - {"ModelManager_ActiveBundle", PERSISTENT}, - {"ModelManager_ClearCache", CLEAR_ON_MANAGER_START}, - {"ModelManager_DownloadIndex", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"ModelManager_LastSyncTime", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"ModelManager_ModelsCache", PERSISTENT | BACKUP}, + {"ModelManager_ActiveBundle", {PERSISTENT, JSON}}, + {"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}}, + {"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT, "0"}}, + {"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, + {"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}}, // Neural Network Lateral Control - {"NeuralNetworkLateralControl", PERSISTENT | BACKUP}, + {"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}}, // sunnylink params - {"EnableSunnylinkUploader", PERSISTENT | BACKUP}, - {"LastSunnylinkPingTime", CLEAR_ON_MANAGER_START}, - {"SunnylinkCache_Roles", PERSISTENT}, - {"SunnylinkCache_Users", PERSISTENT}, - {"SunnylinkDongleId", PERSISTENT}, - {"SunnylinkdPid", PERSISTENT}, - {"SunnylinkEnabled", PERSISTENT}, + {"EnableSunnylinkUploader", {PERSISTENT | BACKUP, BOOL}}, + {"LastSunnylinkPingTime", {CLEAR_ON_MANAGER_START, INT}}, + {"SunnylinkCache_Roles", {PERSISTENT, STRING}}, + {"SunnylinkCache_Users", {PERSISTENT, STRING}}, + {"SunnylinkDongleId", {PERSISTENT, STRING}}, + {"SunnylinkdPid", {PERSISTENT, INT}}, + {"SunnylinkEnabled", {PERSISTENT, BOOL}}, // Backup Manager params - {"BackupManager_CreateBackup", PERSISTENT}, - {"BackupManager_RestoreVersion", PERSISTENT}, + {"BackupManager_CreateBackup", {PERSISTENT, BOOL}}, + {"BackupManager_RestoreVersion", {PERSISTENT, STRING}}, // sunnypilot car specific params - {"HyundaiLongitudinalTuning", PERSISTENT | BACKUP}, + {"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}}, - {"DynamicExperimentalControl", PERSISTENT | BACKUP}, - {"BlindSpot", PERSISTENT | BACKUP}, + {"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}}, // model panel params - {"LagdToggle", PERSISTENT | BACKUP}, - {"LagdToggleDesc", PERSISTENT}, - {"LagdToggledelay", PERSISTENT | BACKUP}, + {"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}}, + {"LagdToggleDelay", {PERSISTENT | BACKUP, FLOAT, "0.2"}}, + {"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}}, // mapd - {"MapAdvisorySpeedLimit", CLEAR_ON_ONROAD_TRANSITION}, - {"MapdVersion", PERSISTENT}, - {"MapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION}, - {"NextMapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION}, - {"Offroad_OSMUpdateRequired", CLEAR_ON_MANAGER_START}, - {"OsmDbUpdatesCheck", CLEAR_ON_MANAGER_START}, // mapd database update happens with device ON, reset on boot - {"OSMDownloadBounds", PERSISTENT}, - {"OsmDownloadedDate", PERSISTENT}, - {"OSMDownloadLocations", PERSISTENT}, - {"OSMDownloadProgress", CLEAR_ON_MANAGER_START}, - {"OsmLocal", PERSISTENT}, - {"OsmLocationName", PERSISTENT}, - {"OsmLocationTitle", PERSISTENT}, - {"OsmLocationUrl", PERSISTENT}, - {"OsmStateName", PERSISTENT}, - {"OsmStateTitle", PERSISTENT}, - {"OsmWayTest", PERSISTENT}, - {"RoadName", CLEAR_ON_ONROAD_TRANSITION}, + {"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}}, + {"MapdVersion", {PERSISTENT, STRING, ""}}, + {"MapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT, "0.0"}}, + {"NextMapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_OSMUpdateRequired", {CLEAR_ON_MANAGER_START, JSON}}, + {"OsmDbUpdatesCheck", {CLEAR_ON_MANAGER_START, BOOL}}, // mapd database update happens with device ON, reset on boot + {"OSMDownloadBounds", {PERSISTENT, STRING}}, + {"OsmDownloadedDate", {PERSISTENT, STRING, "0.0"}}, + {"OSMDownloadLocations", {PERSISTENT, JSON}}, + {"OSMDownloadProgress", {CLEAR_ON_MANAGER_START, JSON}}, + {"OsmLocal", {PERSISTENT, BOOL}}, + {"OsmLocationName", {PERSISTENT, STRING}}, + {"OsmLocationTitle", {PERSISTENT, STRING}}, + {"OsmLocationUrl", {PERSISTENT, STRING}}, + {"OsmStateName", {PERSISTENT, STRING, "All"}}, + {"OsmStateTitle", {PERSISTENT, STRING}}, + {"OsmWayTest", {PERSISTENT, STRING}}, + {"RoadName", {CLEAR_ON_ONROAD_TRANSITION, STRING, ""}}, }; diff --git a/common/params_pyx.pyx b/common/params_pyx.pyx index e851020d83..bffa89b5d3 100644 --- a/common/params_pyx.pyx +++ b/common/params_pyx.pyx @@ -1,19 +1,35 @@ # distutils: language = c++ # cython: language_level = 3 +import builtins +import datetime +import json from libcpp cimport bool from libcpp.string cimport string from libcpp.vector cimport vector +from libcpp.optional cimport optional + +from openpilot.common.swaglog import cloudlog cdef extern from "common/params.h": - cpdef enum ParamKeyType: + cpdef enum ParamKeyFlag: PERSISTENT CLEAR_ON_MANAGER_START CLEAR_ON_ONROAD_TRANSITION CLEAR_ON_OFFROAD_TRANSITION DEVELOPMENT_ONLY + CLEAR_ON_IGNITION_ON BACKUP ALL + cpdef enum ParamKeyType: + STRING + BOOL + INT + FLOAT + TIME + JSON + BYTES + cdef cppclass c_Params "Params": c_Params(string) except + nogil string get(string, bool) nogil @@ -24,10 +40,31 @@ cdef extern from "common/params.h": void putBoolNonBlocking(string, bool) nogil int putBool(string, bool) nogil bool checkKey(string) nogil + ParamKeyType getKeyType(string) nogil + optional[string] getKeyDefaultValue(string) nogil string getParamPath(string) nogil - void clearAll(ParamKeyType) - vector[string] allKeys(ParamKeyType) + void clearAll(ParamKeyFlag) + vector[string] allKeys(ParamKeyFlag) +PYTHON_2_CPP = { + (str, STRING): lambda v: v, + (builtins.bool, BOOL): lambda v: "1" if v else "0", + (int, INT): str, + (float, FLOAT): str, + (datetime.datetime, TIME): lambda v: v.isoformat(), + (dict, JSON): json.dumps, + (list, JSON): json.dumps, + (bytes, BYTES): lambda v: v, +} +CPP_2_PYTHON = { + STRING: lambda v: v.decode("utf-8"), + BOOL: lambda v: v == b"1", + INT: int, + FLOAT: float, + TIME: lambda v: datetime.datetime.fromisoformat(v.decode("utf-8")), + JSON: json.loads, + BYTES: lambda v: v, +} def ensure_bytes(v): return v.encode() if isinstance(v, str) else v @@ -51,8 +88,8 @@ cdef class Params: def __dealloc__(self): del self.p - def clear_all(self, tx_type=ParamKeyType.ALL): - self.p.clearAll(tx_type) + def clear_all(self, tx_flag=ParamKeyFlag.ALL): + self.p.clearAll(tx_flag) def check_key(self, key): key = ensure_bytes(key) @@ -60,21 +97,38 @@ cdef class Params: raise UnknownKeyName(key) return key - def get(self, key, bool block=False, encoding=None): + def python2cpp(self, proposed_type, expected_type, value, key): + cast = PYTHON_2_CPP.get((proposed_type, expected_type)) + if cast: + return cast(value) + raise TypeError(f"Type mismatch while writing param {key}: {proposed_type=} {expected_type=} {value=}") + + def _cpp2python(self, t, value, default, key): + if value is None: + return None + try: + return CPP_2_PYTHON[t](value) + except (KeyError, TypeError, ValueError): + cloudlog.warning(f"Failed to cast param {key} with {value=} from type {t=}") + return self._cpp2python(t, default, None, key) + + def get(self, key, bool block=False, bool return_default=False): cdef string k = self.check_key(key) + cdef ParamKeyType t = self.p.getKeyType(k) + cdef optional[string] default = self.p.getKeyDefaultValue(k) cdef string val with nogil: val = self.p.get(k, block) + default_val = (default.value() if default.has_value() else None) if return_default else None if val == b"": if block: # If we got no value while running in blocked mode # it means we got an interrupt while waiting raise KeyboardInterrupt else: - return None - - return val if encoding is None else val.decode(encoding) + return self._cpp2python(t, default_val, None, key) + return self._cpp2python(t, val, default_val, key) def get_bool(self, key, bool block=False): cdef string k = self.check_key(key) @@ -83,6 +137,11 @@ cdef class Params: r = self.p.getBool(k, block) return r + def _put_cast(self, key, dat): + cdef string k = self.check_key(key) + cdef ParamKeyType t = self.p.getKeyType(k) + return ensure_bytes(self.python2cpp(type(dat), t, dat, key)) + def put(self, key, dat): """ Warning: This function blocks until the param is written to disk! @@ -91,7 +150,7 @@ cdef class Params: in general try to avoid writing params as much as possible. """ cdef string k = self.check_key(key) - cdef string dat_bytes = ensure_bytes(dat) + cdef string dat_bytes = self._put_cast(key, dat) with nogil: self.p.put(k, dat_bytes) @@ -102,7 +161,7 @@ cdef class Params: def put_nonblocking(self, key, dat): cdef string k = self.check_key(key) - cdef string dat_bytes = ensure_bytes(dat) + cdef string dat_bytes = self._put_cast(key, dat) with nogil: self.p.putNonBlocking(k, dat_bytes) @@ -120,5 +179,19 @@ cdef class Params: cdef string key_bytes = ensure_bytes(key) return self.p.getParamPath(key_bytes).decode("utf-8") - def all_keys(self, type=ParamKeyType.ALL): - return self.p.allKeys(type) + def get_type(self, key): + return self.p.getKeyType(self.check_key(key)) + + def all_keys(self, flag=ParamKeyFlag.ALL): + return self.p.allKeys(flag) + + def get_default_value(self, key): + cdef string k = self.check_key(key) + cdef ParamKeyType t = self.p.getKeyType(k) + cdef optional[string] default = self.p.getKeyDefaultValue(k) + return self._cpp2python(t, default.value(), None, key) if default.has_value() else None + + def cpp2python(self, key, value): + cdef string k = self.check_key(key) + cdef ParamKeyType t = self.p.getKeyType(k) + return self._cpp2python(t, value, None, key) diff --git a/common/prefix.py b/common/prefix.py index 762ae70fb4..207f8477d7 100644 --- a/common/prefix.py +++ b/common/prefix.py @@ -9,20 +9,19 @@ from openpilot.system.hardware.hw import Paths from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT class OpenpilotPrefix: - def __init__(self, prefix: str = None, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): + def __init__(self, prefix: str = None, create_dirs_on_enter: bool = True, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): self.prefix = prefix if prefix else str(uuid.uuid4().hex[0:15]) self.msgq_path = os.path.join(Paths.shm_path(), self.prefix) + self.create_dirs_on_enter = create_dirs_on_enter self.clean_dirs_on_exit = clean_dirs_on_exit self.shared_download_cache = shared_download_cache def __enter__(self): self.original_prefix = os.environ.get('OPENPILOT_PREFIX', None) os.environ['OPENPILOT_PREFIX'] = self.prefix - try: - os.mkdir(self.msgq_path) - except FileExistsError: - pass - os.makedirs(Paths.log_root(), exist_ok=True) + + if self.create_dirs_on_enter: + self.create_dirs() if self.shared_download_cache: os.environ["COMMA_CACHE"] = DEFAULT_DOWNLOAD_CACHE_ROOT @@ -40,6 +39,13 @@ class OpenpilotPrefix: pass return False + def create_dirs(self): + try: + os.mkdir(self.msgq_path) + except FileExistsError: + pass + os.makedirs(Paths.log_root(), exist_ok=True) + def clean_dirs(self): symlink_path = Params().get_param_path() if os.path.exists(symlink_path): diff --git a/common/tests/test_params.py b/common/tests/test_params.py index 16cbc45295..592bf2c4b2 100644 --- a/common/tests/test_params.py +++ b/common/tests/test_params.py @@ -1,10 +1,11 @@ import pytest +import datetime import os import threading import time import uuid -from openpilot.common.params import Params, ParamKeyType, UnknownKeyName +from openpilot.common.params import Params, ParamKeyFlag, UnknownKeyName class TestParams: def setup_method(self): @@ -12,7 +13,7 @@ class TestParams: def test_params_put_and_get(self): self.params.put("DongleId", "cb38263377b873ee") - assert self.params.get("DongleId") == b"cb38263377b873ee" + assert self.params.get("DongleId") == "cb38263377b873ee" def test_params_non_ascii(self): st = b"\xe1\x90\xff" @@ -20,7 +21,7 @@ class TestParams: assert self.params.get("CarParams") == st def test_params_get_cleared_manager_start(self): - self.params.put("CarParams", "test") + self.params.put("CarParams", b"test") self.params.put("DongleId", "cb38263377b873ee") assert self.params.get("CarParams") == b"test" @@ -29,24 +30,24 @@ class TestParams: f.write("test") assert os.path.isfile(undefined_param) - self.params.clear_all(ParamKeyType.CLEAR_ON_MANAGER_START) + self.params.clear_all(ParamKeyFlag.CLEAR_ON_MANAGER_START) assert self.params.get("CarParams") is None assert self.params.get("DongleId") is not None assert not os.path.isfile(undefined_param) def test_params_two_things(self): self.params.put("DongleId", "bob") - self.params.put("AthenadPid", "123") - assert self.params.get("DongleId") == b"bob" - assert self.params.get("AthenadPid") == b"123" + self.params.put("AthenadPid", 123) + assert self.params.get("DongleId") == "bob" + assert self.params.get("AthenadPid") == 123 def test_params_get_block(self): def _delayed_writer(): time.sleep(0.1) - self.params.put("CarParams", "test") + self.params.put("CarParams", b"test") threading.Thread(target=_delayed_writer).start() assert self.params.get("CarParams") is None - assert self.params.get("CarParams", True) == b"test" + assert self.params.get("CarParams", block=True) == b"test" def test_params_unknown_key_fails(self): with pytest.raises(UnknownKeyName): @@ -76,17 +77,17 @@ class TestParams: self.params.put_bool("IsMetric", False) assert not self.params.get_bool("IsMetric") - self.params.put("IsMetric", "1") + self.params.put("IsMetric", True) assert self.params.get_bool("IsMetric") - self.params.put("IsMetric", "0") + self.params.put("IsMetric", False) assert not self.params.get_bool("IsMetric") def test_put_non_blocking_with_get_block(self): q = Params() def _delayed_writer(): time.sleep(0.1) - Params().put_nonblocking("CarParams", "test") + Params().put_nonblocking("CarParams", b"test") threading.Thread(target=_delayed_writer).start() assert q.get("CarParams") is None assert q.get("CarParams", True) == b"test" @@ -107,3 +108,34 @@ class TestParams: assert len(keys) > 20 assert len(keys) == len(set(keys)) assert b"CarParams" in keys + + def test_params_default_value(self): + self.params.remove("LanguageSetting") + self.params.remove("LongitudinalPersonality") + self.params.remove("LiveParameters") + + assert self.params.get("LanguageSetting") is None + assert self.params.get("LanguageSetting", return_default=False) is None + assert isinstance(self.params.get("LanguageSetting", return_default=True), str) + assert isinstance(self.params.get("LongitudinalPersonality", return_default=True), int) + assert self.params.get("LiveParameters") is None + assert self.params.get("LiveParameters", return_default=True) is None + + def test_params_get_type(self): + # json + self.params.put("ApiCache_FirehoseStats", {"a": 0}) + assert self.params.get("ApiCache_FirehoseStats") == {"a": 0} + + # int + self.params.put("BootCount", 1441) + assert self.params.get("BootCount") == 1441 + + # bool + self.params.put("AdbEnabled", True) + assert self.params.get("AdbEnabled") + assert isinstance(self.params.get("AdbEnabled"), bool) + + # time + now = datetime.datetime.now(datetime.UTC) + self.params.put("InstallDate", now) + assert self.params.get("InstallDate") == now diff --git a/common/util.cc b/common/util.cc index a853faed04..26a2bd60bc 100644 --- a/common/util.cc +++ b/common/util.cc @@ -1,4 +1,5 @@ #include "common/util.h" +#include "common/swaglog.h" #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #ifdef __linux__ #include @@ -78,8 +80,9 @@ std::string read_file(const std::string& fn) { std::ifstream f(fn, std::ios::binary | std::ios::in); if (f.is_open()) { f.seekg(0, std::ios::end); - int size = f.tellg(); - if (f.good() && size > 0) { + std::streamsize size = f.tellg(); + // seekg and tellg on a directory doesn't return pos_type(-1) but max(streamsize) + if (f.good() && size > 0 && size < std::numeric_limits::max()) { std::string result(size, '\0'); f.seekg(0, std::ios::beg); f.read(result.data(), size); @@ -149,11 +152,16 @@ int safe_fflush(FILE *stream) { return ret; } -int safe_ioctl(int fd, unsigned long request, void *argp) { +int safe_ioctl(int fd, unsigned long request, void *argp, const char* exception_msg) { int ret; do { ret = ioctl(fd, request, argp); } while ((ret == -1) && (errno == EINTR)); + + if (ret == -1 && exception_msg) { + LOGE("safe_ioctl error: %s %s(%d) (fd: %d request: %lx argp: %p)", exception_msg, strerror(errno), errno, fd, request, argp); + throw std::runtime_error(exception_msg); + } return ret; } diff --git a/common/util.h b/common/util.h index 4fb4b13fae..f46db4d9fa 100644 --- a/common/util.h +++ b/common/util.h @@ -88,7 +88,7 @@ int write_file(const char* path, const void* data, size_t size, int flags = O_WR FILE* safe_fopen(const char* filename, const char* mode); size_t safe_fwrite(const void * ptr, size_t size, size_t count, FILE * stream); int safe_fflush(FILE *stream); -int safe_ioctl(int fd, unsigned long request, void *argp); +int safe_ioctl(int fd, unsigned long request, void *argp, const char* exception_msg = nullptr); std::string readlink(const std::string& path); bool file_exists(const std::string& fn); diff --git a/docs/CARS.md b/docs/CARS.md index 9403f7a88b..acd68ace84 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,329 +4,331 @@ A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. -# 319 Supported Cars +# 321 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video|Setup Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| -|Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Acura|ILX 2019|All|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Acura|RDX 2016-18|AcuraWatch Plus or Advance Package|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Acura|RDX 2019-21|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chrysler|Pacifica 2021-23|All|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chrysler|Pacifica Hybrid 2019-24|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None||| -|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=uUGkH6C_EQU| -|Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=uUGkH6C_EQU| -|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=uUGkH6C_EQU| -|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=MewJc9LYp9M| -|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=MewJc9LYp9M| -|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=MewJc9LYp9M| -|Ford|Focus 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Focus Hybrid 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Kuga Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Kuga Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=uUGkH6C_EQU| -|Ford|Kuga Plug-in Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=uUGkH6C_EQU| -|Ford|Maverick 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Mustang Mach-E 2021-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=uUGkH6C_EQU| -|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=uUGkH6C_EQU| -|Genesis|G70 2018|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G70 2019-21|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G70 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G80 2017|All|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G80 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G80 (2.5T Advanced Trim, with HDA II) 2024[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G90 2017-20|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV60 (Advanced Trim) 2023[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV60 (Performance Trim) 2022-23[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV70 (2.5T Trim, without HDA II) 2022-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV70 (3.5T Trim, without HDA II) 2022-23[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV70 Electrified (Australia Only) 2022[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV70 Electrified (with HDA II) 2023-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV80 2023[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Accord 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Accord Hybrid 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[5](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic Hatchback 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic Hatchback Hybrid 2025|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|CR-V 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|e 2020|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|HR-V 2019-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|HR-V 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Insight 2019-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Inspire 2018|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Passport 2019-25|All|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Ridgeline 2017-25|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Azera 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Azera Hybrid 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Azera Hybrid 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Custin 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Elantra 2017-18|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Elantra 2019|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Elantra 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Elantra GT 2017-20|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq 5 (Southeast Asia and Europe only) 2022-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq 5 (with HDA II) 2022-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq 5 (without HDA II) 2022-24[6](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq 6 (with HDA II) 2023-24[6](#footnotes)|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|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)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq Electric 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq Hybrid 2017-19|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq Hybrid 2020-22|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Kona 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Kona Electric (with HDA II, Korea only) 2023[6](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai R connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Kona Hybrid 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai I connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Nexo 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Palisade 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Santa Cruz 2022-24[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Santa Fe 2019-20|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Santa Fe 2021-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Santa Fe Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Santa Fe Plug-in Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Sonata 2018-19|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Sonata 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Sonata Hybrid 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Staria 2023[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Tucson 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Tucson 2022[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Tucson 2023-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Tucson Diesel 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Tucson Hybrid 2022-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Tucson Plug-in Hybrid 2024[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|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)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|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)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Carnival 2022-24[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Carnival (China only) 2023[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|EV6 (Southeast Asia only) 2022-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|EV6 (with HDA II) 2022-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|EV6 (without HDA II) 2022-24[6](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Forte 2019-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Forte 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|K5 2021-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|K8 Hybrid (with HDA II) 2023[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro EV 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro EV 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro EV 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro EV 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro EV (with HDA II) 2025[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai R connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro EV (without HDA II) 2023-25[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Hybrid 2018|All|Stock|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Hybrid 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Hybrid 2023[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Plug-in Hybrid 2018-19|All|Stock|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Plug-in Hybrid 2020|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Plug-in Hybrid 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Plug-in Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Seltos 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sorento 2021-23[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sorento Hybrid 2021-23[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sorento Plug-in Hybrid 2022-23[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sportage 2023-24[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sportage Hybrid 2023[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Stinger 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Telluride 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|ES 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|ES Hybrid 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|LC 2024-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|NX 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|NX Hybrid 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|NX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RC 2018-20|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RC 2023|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RX 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RX 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RX 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RX Hybrid 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RX Hybrid 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|RX Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lexus|UX Hybrid 2019-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Nissan[7](#footnotes)|Altima 2019-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan B connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Nissan[7](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Nissan[7](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Nissan[7](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Ram connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ram connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ram connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Rivian A connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://youtu.be/uaISd1j7Z4U| -|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Rivian A connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://youtu.be/uaISd1j7Z4U| -|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Subaru|Ascent 2019-21|All[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Forester 2017-18|EyeSight Driver Assistance[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Forester 2019-21|All[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Impreza 2017-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Impreza 2020-22|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Legacy 2015-18|EyeSight Driver Assistance[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Legacy 2020-22|All[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Outback 2015-17|EyeSight Driver Assistance[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Outback 2018-19|EyeSight Driver Assistance[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Outback 2020-22|All[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|XV 2018-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|XV 2020-21|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Škoda|Fabia 2022-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| -|Škoda|Kamiq 2021-23[13,15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| -|Škoda|Karoq 2019-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Škoda|Kodiaq 2017-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Škoda|Octavia 2015-19[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Škoda|Octavia RS 2016[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Škoda|Octavia Scout 2017-19[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Škoda|Scala 2020-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| -|Škoda|Superb 2015-22[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Tesla[11](#footnotes)|Model 3 (with HW3) 2019-23[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Tesla[11](#footnotes)|Model 3 (with HW4) 2024-25[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Tesla[11](#footnotes)|Model Y (with HW3) 2020-23[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Tesla[11](#footnotes)|Model Y (with HW4) 2024[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Avalon 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Avalon 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Avalon 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Avalon 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Avalon Hybrid 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Avalon Hybrid 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|C-HR 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|C-HR 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|C-HR Hybrid 2021-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Camry 2018-20|All|Stock|0 mph[12](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Camry 2021-24|All|openpilot|0 mph[12](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Camry Hybrid 2021-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Corolla 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Corolla Hybrid (South America only) 2020-23|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Highlander 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Highlander 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Highlander Hybrid 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Highlander Hybrid 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Mirai 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Prius 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Prius 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Prius Prime 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Prius v 2017|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 Hybrid 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Toyota|Sienna 2018-20|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Jetta 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Passat 2015-22[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| -|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| -|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| -|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Acura|ILX 2019|All|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Acura|RDX 2016-18|AcuraWatch Plus or Advance Package|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Acura|RDX 2019-21|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Chrysler|Pacifica 2021-23|All|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None||| +|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Focus 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Focus Hybrid 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Kuga Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Kuga Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Kuga Plug-in Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Maverick 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Mustang Mach-E 2021-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|G70 2018|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|G70 2019-21|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|G70 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|G80 2017|All|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|G80 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|G80 (2.5T Advanced Trim, with HDA II) 2024[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|G90 2017-20|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|GV60 (Advanced Trim) 2023[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|GV60 (Performance Trim) 2022-23[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|GV70 (2.5T Trim, without HDA II) 2022-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|GV70 (3.5T Trim, without HDA II) 2022-23[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|GV70 Electrified (Australia Only) 2022[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|GV70 Electrified (with HDA II) 2023-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|GV80 2023[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Accord 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Accord 2023|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Accord Hybrid 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[5](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Civic 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Civic Hatchback 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Civic Hatchback Hybrid 2025|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Civic Hybrid 2025|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|CR-V 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|15 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|e 2020|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|HR-V 2019-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|HR-V 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Insight 2019-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Inspire 2018|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Passport 2019-25|All|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Ridgeline 2017-25|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Azera 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Azera Hybrid 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Azera Hybrid 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Custin 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Elantra 2017-18|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Elantra 2019|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Elantra 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Elantra GT 2017-20|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Ioniq 5 (Southeast Asia and Europe only) 2022-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Ioniq 5 (with HDA II) 2022-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Ioniq 5 (without HDA II) 2022-24[6](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Ioniq 6 (with HDA II) 2023-24[6](#footnotes)|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|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)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Ioniq Electric 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Ioniq Hybrid 2017-19|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Ioniq Hybrid 2020-22|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Kona 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Kona Electric (with HDA II, Korea only) 2023[6](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai R connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Kona Hybrid 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai I connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Nexo 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Palisade 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Santa Cruz 2022-24[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Santa Fe 2019-20|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Santa Fe 2021-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Santa Fe Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Santa Fe Plug-in Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Sonata 2018-19|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Sonata 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Sonata Hybrid 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Staria 2023[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Tucson 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Tucson 2022[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Tucson 2023-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Tucson Diesel 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Tucson Hybrid 2022-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Tucson Plug-in Hybrid 2024[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|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)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|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)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Carnival 2022-24[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Carnival (China only) 2023[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|EV6 (Southeast Asia only) 2022-24[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|EV6 (with HDA II) 2022-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|EV6 (without HDA II) 2022-24[6](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Forte 2019-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Forte 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|K5 2021-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|K8 Hybrid (with HDA II) 2023[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro EV 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro EV 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro EV 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro EV 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro EV (with HDA II) 2025[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai R connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro EV (without HDA II) 2023-25[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro Hybrid 2018|Smart Cruise Control (SCC)|Stock|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro Hybrid 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro Hybrid 2023[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro Plug-in Hybrid 2018-19|All|Stock|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro Plug-in Hybrid 2020|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro Plug-in Hybrid 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro Plug-in Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Seltos 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Sorento 2021-23[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Sorento Hybrid 2021-23[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Sorento Plug-in Hybrid 2022-23[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Sportage 2023-24[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Sportage Hybrid 2023[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Stinger 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Telluride 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|ES 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|ES Hybrid 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|LC 2024-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|NX 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|NX Hybrid 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|NX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RC 2018-20|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RC 2023|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RX 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RX 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RX 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RX Hybrid 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RX Hybrid 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RX Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|UX Hybrid 2019-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Nissan[7](#footnotes)|Altima 2019-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan B connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Nissan[7](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Nissan[7](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Nissan[7](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Ram connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ram connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ram connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Rivian A connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Rivian A connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Subaru|Ascent 2019-21|All[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Forester 2017-18|EyeSight Driver Assistance[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Forester 2019-21|All[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Impreza 2017-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Impreza 2020-22|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Legacy 2015-18|EyeSight Driver Assistance[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Legacy 2020-22|All[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Outback 2015-17|EyeSight Driver Assistance[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Outback 2018-19|EyeSight Driver Assistance[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Outback 2020-22|All[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|XV 2018-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|XV 2020-21|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Škoda|Fabia 2022-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| +|Škoda|Kamiq 2021-23[13,15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| +|Škoda|Karoq 2019-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Škoda|Kodiaq 2017-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Škoda|Octavia 2015-19[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Škoda|Octavia RS 2016[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Škoda|Octavia Scout 2017-19[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Škoda|Scala 2020-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| +|Škoda|Superb 2015-22[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Tesla[11](#footnotes)|Model 3 (with HW3) 2019-23[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Tesla[11](#footnotes)|Model 3 (with HW4) 2024-25[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Tesla[11](#footnotes)|Model Y (with HW3) 2020-23[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Tesla[11](#footnotes)|Model Y (with HW4) 2024[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Avalon 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Avalon 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Avalon 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Avalon 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Avalon Hybrid 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Avalon Hybrid 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|C-HR 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|C-HR 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|C-HR Hybrid 2021-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Camry 2018-20|All|Stock|0 mph[12](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Camry 2021-24|All|openpilot|0 mph[12](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Camry Hybrid 2021-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Corolla 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Corolla Hybrid (South America only) 2020-23|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Highlander 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Highlander 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Highlander Hybrid 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Highlander Hybrid 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Mirai 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Prius 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Prius 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Prius Prime 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Prius v 2017|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|RAV4 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|RAV4 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|RAV4 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|RAV4 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|RAV4 Hybrid 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|RAV4 Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Sienna 2018-20|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Jetta 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Passat 2015-22[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| +|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| +|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| +|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| ### Footnotes 1openpilot Longitudinal Control (Alpha) is available behind a toggle; the toggle is only available in non-release branches such as `devel` or `nightly-dev`.
diff --git a/launch_env.sh b/launch_env.sh index b0bff4e57f..0ed1395b37 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="12.4" + export AGNOS_VERSION="12.6" fi export STAGING_ROOT="/data/safe_staging" diff --git a/opendbc_repo b/opendbc_repo index 2a11f9627c..5da91a7ae6 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 2a11f9627cf7e43c0d7b0ab25051b7b305f03c1f +Subproject commit 5da91a7ae65849d2506c772e45239915ed380b19 diff --git a/panda b/panda index c597f948f9..0e7a3fd8cf 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit c597f948f99f09e885f5f85c7da1745e92222a6e +Subproject commit 0e7a3fd8cf6f7a08b80ca15cea1b3989fc11b135 diff --git a/pyproject.toml b/pyproject.toml index bc1b2a0644..0968cd4ed3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "pycapnp", "Cython", "setuptools", - "numpy >=2.0, <2.2", # Linting issues with mypy in 2.2 + "numpy >=2.0", # body / webrtcd "aiohttp", @@ -42,7 +42,6 @@ dependencies = [ # modeld "onnx >= 1.14.0", - "llvmlite", # logging "pyzmq", @@ -86,8 +85,8 @@ testing = [ "pytest", "pytest-cpp", "pytest-subtests", - # https://github.com/pytest-dev/pytest-xdist/issues/1215 - "pytest-xdist @ git+https://github.com/sshane/pytest-xdist@909e97b49d12401c10608f9d777bfc9dab8a4413", + # https://github.com/pytest-dev/pytest-xdist/pull/1229 + "pytest-xdist @ git+https://github.com/sshane/pytest-xdist@2b4372bd62699fb412c4fe2f95bf9f01bd2018da", "pytest-timeout", "pytest-randomly", "pytest-asyncio", @@ -121,7 +120,6 @@ dev = [ tools = [ "metadrive-simulator @ https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl ; (platform_machine != 'aarch64')", - "rerun-sdk >= 0.18", ] [project.urls] @@ -153,6 +151,7 @@ markers = [ testpaths = [ "common", "selfdrive", + "system/manager", "system/updated", "system/athena", "system/camerad", @@ -178,9 +177,6 @@ skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, . [tool.mypy] python_version = "3.11" -plugins = [ - "numpy.typing.mypy_plugin", -] exclude = [ "cereal/", "msgq/", @@ -263,6 +259,7 @@ lint.flake8-implicit-str-concat.allow-multiline = false "pytest.main".msg = "pytest.main requires special handling that is easy to mess up!" "unittest".msg = "Use pytest" "pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure" +"time.time".msg = "Use time.monotonic" [tool.ruff.format] quote-style = "preserve" diff --git a/release/README.md b/release/README.md index 06404be6ab..0ceb765597 100644 --- a/release/README.md +++ b/release/README.md @@ -4,20 +4,24 @@ ## release checklist **Go to `devel-staging`** +- [ ] make issue to track release - [ ] update RELEASES.md -- [ ] update `devel-staging`: `git reset --hard origin/master-ci` +- [ ] trigger new nightly build: https://github.com/commaai/openpilot/actions/workflows/release.yaml +- [ ] update `devel-staging`: `git reset --hard origin/__nightly` +- [ ] build new userdata partition from `release3-staging` - [ ] open a pull request from `devel-staging` to `devel` -- [ ] post on Discord +- [ ] post on Discord, tag `@release crew` **Go to `devel`** - [ ] bump version on master: `common/version.h` and `RELEASES.md` -- [ ] before merging the pull request +- [ ] before merging the pull request, test the following: - [ ] update from previous release -> new release - [ ] update from new release -> previous release - [ ] fresh install with `openpilot-test.comma.ai` - [ ] drive on fresh install - [ ] no submodules or LFS - [ ] check sentry, MTBF, etc. + - [ ] stress test in production **Go to `release3`** - [ ] publish the blog post diff --git a/release/build_devel.sh b/release/build_devel.sh index 05dcaafcaa..6167bda7ba 100755 --- a/release/build_devel.sh +++ b/release/build_devel.sh @@ -33,7 +33,7 @@ git clean -xdff git lfs uninstall # remove everything except .git -echo "[-] erasing old openpilot T=$SECONDS" +echo "[-] erasing old sunnypilot T=$SECONDS" find . -maxdepth 1 -not -path './.git' -not -name '.' -not -name '..' -exec rm -rf '{}' \; # reset source tree @@ -61,7 +61,7 @@ echo -n "$GIT_COMMIT_DATE" > git_src_commit_date echo "[-] committing version $VERSION T=$SECONDS" git add -f . git status -git commit -a -m "openpilot v$VERSION release +git commit -a -m "sunnypilot v$VERSION release date: $DATETIME master commit: $GIT_HASH diff --git a/release/check-submodules.sh b/release/check-submodules.sh index 93869a7403..7120fa15d8 100755 --- a/release/check-submodules.sh +++ b/release/check-submodules.sh @@ -1,17 +1,43 @@ #!/usr/bin/env bash +has_submodule_changes() { + local submodule_path="$1" + if [ -n "$SUBMODULE_PATHS" ]; then + echo "$SUBMODULE_PATHS" | grep -q "$submodule_path" + return $? + fi + return 1 +} + while read hash submodule ref; do + if [ -z "$hash" ] || [ -z "$submodule" ]; then + continue + fi + + hash=$(echo "$hash" | sed 's/^[+-]//') + if [ "$submodule" = "tinygrad_repo" ]; then echo "Skipping $submodule" continue fi - git -C $submodule fetch --depth 100 origin master - git -C $submodule branch -r --contains $hash | grep "origin/master" - if [ "$?" -eq 0 ]; then - echo "$submodule ok" + if [ "$CHECK_PR_REFS" = "true" ] && has_submodule_changes "$submodule"; then + echo "Checking $submodule (non-master): verifying hash $hash exists" + git -C $submodule fetch --depth 100 origin + if git -C $submodule cat-file -e $hash 2>/dev/null; then + echo "$submodule ok (hash exists)" + else + echo "$submodule: $hash does not exist in the repository" + exit 1 + fi else - echo "$submodule: $hash is not on master" - exit 1 + git -C $submodule fetch --depth 100 origin master + git -C $submodule branch -r --contains $hash | grep "origin/master" + if [ "$?" -eq 0 ]; then + echo "$submodule ok" + else + echo "$submodule: $hash is not on master" + exit 1 + fi fi done <<< $(git submodule status --recursive) diff --git a/release/ci/docker_build_sp.sh b/release/ci/docker_build_sp.sh new file mode 100755 index 0000000000..9a8d390c61 --- /dev/null +++ b/release/ci/docker_build_sp.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -e + +# To build sim and docs, you can run the following to mount the scons cache to the same place as in CI: +# mkdir -p .ci_cache/scons_cache +# sudo mount --bind /tmp/scons_cache/ .ci_cache/scons_cache + +SCRIPT_DIR=$(dirname "$0") +OPENPILOT_DIR=$SCRIPT_DIR/../../ +if [ -n "$TARGET_ARCHITECTURE" ]; then + PLATFORM="linux/$TARGET_ARCHITECTURE" + TAG_SUFFIX="-$TARGET_ARCHITECTURE" +else + PLATFORM="linux/$(uname -m)" + TAG_SUFFIX="" +fi + +source $SCRIPT_DIR/docker_common_sp.sh $1 "$TAG_SUFFIX" + +DOCKER_BUILDKIT=1 docker buildx build --provenance false --pull --platform $PLATFORM --load --cache-to type=inline --cache-from type=registry,ref=$REMOTE_TAG -t $DOCKER_IMAGE:latest -t $REMOTE_TAG -t $LOCAL_TAG -f $OPENPILOT_DIR/$DOCKER_FILE $OPENPILOT_DIR + +if [ -n "$PUSH_IMAGE" ]; then + docker push $REMOTE_TAG + docker tag $REMOTE_TAG $REMOTE_SHA_TAG + docker push $REMOTE_SHA_TAG +fi diff --git a/release/ci/docker_common_sp.sh b/release/ci/docker_common_sp.sh new file mode 100755 index 0000000000..e8f515762a --- /dev/null +++ b/release/ci/docker_common_sp.sh @@ -0,0 +1,18 @@ +if [ "$1" = "base" ]; then + export DOCKER_IMAGE=sunnypilot-base + export DOCKER_FILE=Dockerfile.sunnypilot_base +elif [ "$1" = "prebuilt" ]; then + export DOCKER_IMAGE=sunnypilot-prebuilt + export DOCKER_FILE=Dockerfile.sunnypilot +else + echo "Invalid docker build image: '$1'" + exit 1 +fi + +export DOCKER_REGISTRY=ghcr.io/sunnypilot +export COMMIT_SHA=$(git rev-parse HEAD) + +TAG_SUFFIX=$2 +LOCAL_TAG=$DOCKER_IMAGE$TAG_SUFFIX +REMOTE_TAG=$DOCKER_REGISTRY/$LOCAL_TAG +REMOTE_SHA_TAG=$DOCKER_REGISTRY/$LOCAL_TAG:$COMMIT_SHA diff --git a/release/identity.sh b/release/identity.sh index c699c94650..1913df3c5b 100644 --- a/release/identity.sh +++ b/release/identity.sh @@ -1,4 +1,4 @@ -export GIT_COMMITTER_NAME="Vehicle Researcher" -export GIT_COMMITTER_EMAIL="user@comma.ai" -export GIT_AUTHOR_NAME="Vehicle Researcher" -export GIT_AUTHOR_EMAIL="user@comma.ai" +export GIT_COMMITTER_NAME="github-actions[bot]" +export GIT_COMMITTER_EMAIL="github-actions[bot]@users.noreply.github.com" +export GIT_AUTHOR_NAME="github-actions[bot]" +export GIT_AUTHOR_EMAIL="github-actions[bot]@users.noreply.github.com" diff --git a/release/release_files.py b/release/release_files.py index 98b88293b9..0b9e034bf1 100755 --- a/release/release_files.py +++ b/release/release_files.py @@ -17,6 +17,8 @@ blacklist = [ ".gitattributes", ".git$", ".gitmodules", + ".run/", + ".idea/", ] # gets you through the blacklist diff --git a/selfdrive/car/car_specific.py b/selfdrive/car/car_specific.py index 151a8479a7..270111524e 100644 --- a/selfdrive/car/car_specific.py +++ b/selfdrive/car/car_specific.py @@ -57,7 +57,7 @@ class CarSpecificEvents: events.add(EventName.belowSteerSpeed) elif self.CP.brand == 'honda': - events = self.create_common_events(CS, CS_prev, pcm_enable=False) + events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport], pcm_enable=False) if self.CP.pcmCruise and CS.vEgo < self.CP.minEnableSpeed: events.add(EventName.belowEngageSpeed) @@ -78,7 +78,8 @@ class CarSpecificEvents: events.add(EventName.manualRestart) elif self.CP.brand == 'toyota': - events = self.create_common_events(CS, CS_prev) + # TODO: when we check for unexpected disengagement, check gear not S1, S2, S3 + events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport]) if self.CP.openpilotLongitudinalControl: if CS.cruiseState.standstill and not CS.brakePressed: diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 8448af56db..159ab8fb17 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import json import os import time import threading @@ -107,7 +106,7 @@ class Car: with car.CarParams.from_bytes(cached_params_raw) as _cached_params: cached_params = _cached_params - fixed_fingerprint = json.loads(self.params.get("CarPlatformBundle", encoding='utf-8') or "{}").get("platform", None) + fixed_fingerprint = (self.params.get("CarPlatformBundle") or {}).get("platform", None) self.CI = get_car(*self.can_callbacks, obd_callback(self.params), alpha_long_allowed, is_release, num_pandas, cached_params, fixed_fingerprint) sunnypilot_interfaces.setup_interfaces(self.CI, self.params) @@ -147,7 +146,7 @@ class Car: except Exception: pass - secoc_key = self.params.get("SecOCKey", encoding='utf8') + secoc_key = self.params.get("SecOCKey") if secoc_key is not None: saved_secoc_key = bytes.fromhex(secoc_key.strip()) if len(saved_secoc_key) == 16: diff --git a/selfdrive/car/cruise.py b/selfdrive/car/cruise.py index 01ef2d7c99..5ffb43f9e5 100644 --- a/selfdrive/car/cruise.py +++ b/selfdrive/car/cruise.py @@ -2,7 +2,7 @@ import math import numpy as np from cereal import car -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.sunnypilot.selfdrive.car.cruise_ext import VCruiseHelperSP diff --git a/selfdrive/car/tests/test_cruise_speed.py b/selfdrive/car/tests/test_cruise_speed.py index dd78b77680..4f9444d4bb 100644 --- a/selfdrive/car/tests/test_cruise_speed.py +++ b/selfdrive/car/tests/test_cruise_speed.py @@ -6,7 +6,7 @@ from parameterized import parameterized_class from cereal import log from openpilot.selfdrive.car.cruise import VCruiseHelper, V_CRUISE_MIN, V_CRUISE_MAX, V_CRUISE_INITIAL, IMPERIAL_INCREMENT from cereal import car -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver ButtonEvent = car.CarState.ButtonEvent diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index 0fc6716dd3..033370c922 100644 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -5,7 +5,6 @@ import pytest import random import unittest # noqa: TID251 from collections import defaultdict, Counter -from functools import partial import hypothesis.strategies as st from hypothesis import Phase, given, settings from parameterized import parameterized_class @@ -23,8 +22,7 @@ from openpilot.common.basedir import BASEDIR from openpilot.selfdrive.pandad import can_capnp_to_list from openpilot.selfdrive.test.helpers import read_segment_list from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT -from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotci_source_zst, openpilotci_source, internal_source, \ - internal_source_zst, comma_api_source, auto_source +from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotci_source, internal_source, comma_api_source from openpilot.tools.lib.route import SegmentName SafetyModel = car.CarParams.SafetyModel @@ -126,9 +124,8 @@ class TestCarModelBase(unittest.TestCase): segment_range = f"{cls.test_route.route}/{seg}" try: - source = partial(auto_source, sources=[internal_source, internal_source_zst] if len(INTERNAL_SEG_LIST) else \ - [openpilotci_source_zst, openpilotci_source, comma_api_source]) - lr = LogReader(segment_range, source=source, sort_by_time=True) + sources = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source] + lr = LogReader(segment_range, sources=sources, sort_by_time=True) return cls.get_testing_data_from_logreader(lr) except (LogsUnavailable, AssertionError): pass @@ -199,7 +196,6 @@ class TestCarModelBase(unittest.TestCase): def test_car_interface(self): # TODO: also check for checksum violations from can parser can_invalid_cnt = 0 - can_valid = False CC = structs.CarControl().as_reader() CC_SP = structs.CarControlSP() @@ -207,11 +203,8 @@ class TestCarModelBase(unittest.TestCase): CS, _ = self.CI.update(msg) self.CI.apply(CC, CC_SP, msg[0]) - if CS.canValid: - can_valid = True - # wait max of 2s for low frequency msgs to be seen - if i > 200 or can_valid: + if i > 250: can_invalid_cnt += not CS.canValid self.assertEqual(can_invalid_cnt, 0) @@ -331,7 +324,7 @@ class TestCarModelBase(unittest.TestCase): vehicle_speed_seen = self.CP.steerControlType == SteerControlType.angle and not self.CP.notCar - for dat in msgs: + for n, dat in enumerate(msgs): # due to panda updating state selectively, only edges are expected to match # TODO: warm up CarState with real CAN messages to check edge of both sources # (eg. toyota's gasPressed is the inverse of a signal being set) @@ -350,6 +343,8 @@ class TestCarModelBase(unittest.TestCase): can = [(int(time.monotonic() * 1e9), [CanData(address=address, dat=dat, src=bus)])] CS, _ = self.CI.update(can) + if n < 5: # CANParser warmup time + continue if self.safety.get_gas_pressed_prev() != prev_panda_gas: self.assertEqual(CS.gasPressed, self.safety.get_gas_pressed_prev()) @@ -369,7 +364,7 @@ class TestCarModelBase(unittest.TestCase): if self.safety.get_steering_disengage_prev() != prev_panda_steering_disengage: self.assertEqual(CS.steeringDisengage, self.safety.get_steering_disengage_prev()) - if self.safety.get_vehicle_moving() != prev_panda_vehicle_moving: + if self.safety.get_vehicle_moving() != prev_panda_vehicle_moving and not self.CP.notCar: self.assertEqual(not CS.standstill, self.safety.get_vehicle_moving()) # check vehicle speed if angle control car or available @@ -427,7 +422,7 @@ class TestCarModelBase(unittest.TestCase): # TODO: check rest of panda's carstate (steering, ACC main on, etc.) checks['gasPressed'] += CS.gasPressed != self.safety.get_gas_pressed_prev() - checks['standstill'] += CS.standstill == self.safety.get_vehicle_moving() + checks['standstill'] += (CS.standstill == self.safety.get_vehicle_moving()) and not self.CP.notCar # check vehicle speed if angle control car or available if self.safety.get_vehicle_speed_min() > 0 or self.safety.get_vehicle_speed_max() > 0: @@ -444,7 +439,9 @@ class TestCarModelBase(unittest.TestCase): if self.CP.carFingerprint in (HONDA.HONDA_PILOT, HONDA.HONDA_RIDGELINE) and CS.brake > 0.05: brake_pressed = False checks['brakePressed'] += brake_pressed != self.safety.get_brake_pressed_prev() - checks['regenBraking'] += CS.regenBraking != self.safety.get_regen_braking_prev() + # TODO: remove this exception before closing commaai/opendbc#1100 + if not (self.CP.brand == "honda" and self.CP.flags & HondaFlags.BOSCH): + checks['regenBraking'] += CS.regenBraking != self.safety.get_regen_braking_prev() checks['steeringDisengage'] += CS.steeringDisengage != self.safety.get_steering_disengage_prev() if self.CP.pcmCruise: diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 9ef985fcc4..2b4b62c8ee 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -2,11 +2,11 @@ import math import threading import time -from typing import SupportsFloat +from numbers import Number from cereal import car, log import cereal.messaging as messaging -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper from openpilot.common.swaglog import cloudlog @@ -21,6 +21,8 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.selfdrive.controls.lib.longcontrol import LongControl from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose +from openpilot.sunnypilot.livedelay.helpers import get_lat_delay +from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase from openpilot.sunnypilot.selfdrive.controls.controlsd_ext import ControlsExt State = log.SelfdriveState.OpenpilotState @@ -30,15 +32,16 @@ LaneChangeDirection = log.LaneChangeDirection ACTUATOR_FIELDS = tuple(car.CarControl.Actuators.schema.fields.keys()) -class Controls(ControlsExt): +class Controls(ControlsExt, ModelStateBase): def __init__(self) -> None: self.params = Params() cloudlog.info("controlsd is waiting for CarParams") self.CP = messaging.log_from_bytes(self.params.get("CarParams", block=True), car.CarParams) cloudlog.info("controlsd got CarParams") - # Initialize sunnypilot controlsd extension + # Initialize sunnypilot controlsd extension and base model state ControlsExt.__init__(self, self.CP, self.params) + ModelStateBase.__init__(self) self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP) @@ -93,8 +96,9 @@ class Controls(ControlsExt): torque_params.frictionCoefficientFiltered) self.LaC.extension.update_model_v2(self.sm['modelV2']) - calculated_lag = self.LaC.extension.lagd_torqued_main(self.CP, self.sm['liveDelay']) - self.LaC.extension.update_lateral_lag(calculated_lag) + + self.lat_delay = get_lat_delay(self.params, self.sm["liveDelay"].lateralDelay) + self.LaC.extension.update_lateral_lag(self.lat_delay) long_plan = self.sm['longitudinalPlan'] model_v2 = self.sm['modelV2'] @@ -143,7 +147,7 @@ class Controls(ControlsExt): # Ensure no NaNs/Infs for p in ACTUATOR_FIELDS: attr = getattr(actuators, p) - if not isinstance(attr, SupportsFloat): + if not isinstance(attr, Number): continue if not math.isfinite(attr): @@ -164,10 +168,7 @@ class Controls(ControlsExt): CC.cruiseControl.override = CC.enabled and not CC.longActive and self.CP.openpilotLongitudinalControl CC.cruiseControl.cancel = CS.cruiseState.enabled and (not CC.enabled or not self.CP.pcmCruise) - - speeds = self.sm['longitudinalPlan'].speeds - if len(speeds): - CC.cruiseControl.resume = CC.enabled and CS.cruiseState.standstill and speeds[-1] > 0.1 + CC.cruiseControl.resume = CC.enabled and CS.cruiseState.standstill and not self.sm['longitudinalPlan'].shouldStop hudControl = CC.hudControl hudControl.setSpeed = float(CS.vCruiseCluster * CV.KPH_TO_MS) diff --git a/selfdrive/controls/lib/desire_helper.py b/selfdrive/controls/lib/desire_helper.py index 23386e2ad7..2adfa65f6d 100644 --- a/selfdrive/controls/lib/desire_helper.py +++ b/selfdrive/controls/lib/desire_helper.py @@ -1,5 +1,5 @@ from cereal import log -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.realtime import DT_MDL from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode diff --git a/selfdrive/controls/lib/drive_helpers.py b/selfdrive/controls/lib/drive_helpers.py index 51eb694b4c..e28fa3021c 100644 --- a/selfdrive/controls/lib/drive_helpers.py +++ b/selfdrive/controls/lib/drive_helpers.py @@ -1,5 +1,5 @@ import numpy as np -from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY +from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.common.realtime import DT_CTRL, DT_MDL MIN_SPEED = 1.0 diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 467f403103..7885902f12 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -2,8 +2,9 @@ import math import numpy as np from cereal import log +from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction from opendbc.car.interfaces import LatControlInputs -from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY +from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.selfdrive.controls.lib.latcontrol import LatControl from openpilot.common.pid import PIDController @@ -61,13 +62,13 @@ class LatControlTorque(LatControl): measurement = actual_lateral_accel + low_speed_factor * actual_curvature gravity_adjusted_lateral_accel = desired_lateral_accel - roll_compensation torque_from_setpoint = self.torque_from_lateral_accel(LatControlInputs(setpoint, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, - setpoint, lateral_accel_deadzone, friction_compensation=False, gravity_adjusted=False) + gravity_adjusted=False) torque_from_measurement = self.torque_from_lateral_accel(LatControlInputs(measurement, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, - measurement, lateral_accel_deadzone, friction_compensation=False, gravity_adjusted=False) + gravity_adjusted=False) pid_log.error = float(torque_from_setpoint - torque_from_measurement) ff = self.torque_from_lateral_accel(LatControlInputs(gravity_adjusted_lateral_accel, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, - desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, friction_compensation=True, gravity_adjusted=True) + ff += get_friction(desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) # Lateral acceleration torque controller extension updates # Overrides stock ff and pid_log.error diff --git a/selfdrive/controls/lib/lateral_mpc_lib/SConscript b/selfdrive/controls/lib/lateral_mpc_lib/SConscript index 2c03da06a6..c9ebf89207 100644 --- a/selfdrive/controls/lib/lateral_mpc_lib/SConscript +++ b/selfdrive/controls/lib/lateral_mpc_lib/SConscript @@ -1,4 +1,4 @@ -Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'opendbc_python', 'np_version') +Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'np_version') gen = "c_generated_code" @@ -55,18 +55,23 @@ source_list = ['lat_mpc.py', ] lenv = env.Clone() +acados_rel_path = Dir(gen).rel_path(Dir(f"#third_party/acados/{arch}/lib")) +lenv["RPATH"] += [lenv.Literal(f'\\$$ORIGIN/{acados_rel_path}')] lenv.Clean(generated_files, Dir(gen)) generated_lat = lenv.Command(generated_files, source_list, f"cd {Dir('.').abspath} && python3 lat_mpc.py") -lenv.Depends(generated_lat, [msgq_python, common_python, opendbc_python]) +lenv.Depends(generated_lat, [msgq_python, common_python]) lenv["CFLAGS"].append("-DACADOS_WITH_QPOASES") lenv["CXXFLAGS"].append("-DACADOS_WITH_QPOASES") lenv["CCFLAGS"].append("-Wno-unused") if arch != "Darwin": lenv["LINKFLAGS"].append("-Wl,--disable-new-dtags") +else: + lenv["LINKFLAGS"].append("-Wl,-install_name,@loader_path/libacados_ocp_solver_lat.dylib") + lenv["LINKFLAGS"].append(f"-Wl,-rpath,@loader_path/{acados_rel_path}") lib_solver = lenv.SharedLibrary(f"{gen}/acados_ocp_solver_lat", build_files, LIBS=['m', 'acados', 'hpipm', 'blasfeo', 'qpOASES_e']) @@ -78,7 +83,8 @@ libacados_ocp_solver_pxd = File(f'{gen}/acados_solver.pxd') libacados_ocp_solver_c = File(f'{gen}/acados_ocp_solver_pyx.c') lenv2 = envCython.Clone() -lenv2["LINKFLAGS"] += [lib_solver[0].get_labspath()] +lenv2["LIBPATH"] += [lib_solver[0].dir.abspath] +lenv2["RPATH"] += [lenv2.Literal('\\$$ORIGIN')] lenv2.Command(libacados_ocp_solver_c, [acados_ocp_solver_pyx, acados_ocp_solver_common, libacados_ocp_solver_pxd], f'cython' + \ @@ -86,6 +92,6 @@ lenv2.Command(libacados_ocp_solver_c, f' -I {libacados_ocp_solver_pxd.get_dir().get_labspath()}' + \ f' -I {acados_ocp_solver_common.get_dir().get_labspath()}' + \ f' {acados_ocp_solver_pyx.get_labspath()}') -lib_cython = lenv2.Program(f'{gen}/acados_ocp_solver_pyx.so', [libacados_ocp_solver_c]) +lib_cython = lenv2.Program(f'{gen}/acados_ocp_solver_pyx.so', [libacados_ocp_solver_c], LIBS=['acados_ocp_solver_lat']) lenv2.Depends(lib_cython, lib_solver) lenv2.Depends(libacados_ocp_solver_c, np_version) diff --git a/selfdrive/controls/lib/ldw.py b/selfdrive/controls/lib/ldw.py index caf03fec73..78a6d6cf6e 100644 --- a/selfdrive/controls/lib/ldw.py +++ b/selfdrive/controls/lib/ldw.py @@ -1,6 +1,6 @@ from cereal import log from openpilot.common.realtime import DT_CTRL -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV CAMERA_OFFSET = 0.04 diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript b/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript index 2a155717c0..164b965142 100644 --- a/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript +++ b/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript @@ -1,4 +1,4 @@ -Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'opendbc_python', 'pandad_python', 'np_version') +Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'pandad_python', 'np_version') gen = "c_generated_code" @@ -61,12 +61,13 @@ source_list = ['long_mpc.py', ] lenv = env.Clone() +acados_rel_path = Dir(gen).rel_path(Dir(f"#third_party/acados/{arch}/lib")) +lenv["RPATH"] += [lenv.Literal(f'\\$$ORIGIN/{acados_rel_path}')] lenv.Clean(generated_files, Dir(gen)) - generated_long = lenv.Command(generated_files, source_list, f"cd {Dir('.').abspath} && python3 long_mpc.py") -lenv.Depends(generated_long, [msgq_python, common_python, opendbc_python, pandad_python]) +lenv.Depends(generated_long, [msgq_python, common_python, pandad_python]) lenv["CFLAGS"].append("-DACADOS_WITH_QPOASES") lenv["CXXFLAGS"].append("-DACADOS_WITH_QPOASES") @@ -75,6 +76,7 @@ if arch != "Darwin": lenv["LINKFLAGS"].append("-Wl,--disable-new-dtags") else: lenv["LINKFLAGS"].append("-Wl,-install_name,@loader_path/libacados_ocp_solver_long.dylib") + lenv["LINKFLAGS"].append(f"-Wl,-rpath,@loader_path/{acados_rel_path}") lib_solver = lenv.SharedLibrary(f"{gen}/acados_ocp_solver_long", build_files, LIBS=['m', 'acados', 'hpipm', 'blasfeo', 'qpOASES_e']) @@ -86,7 +88,8 @@ libacados_ocp_solver_pxd = File(f'{gen}/acados_solver.pxd') libacados_ocp_solver_c = File(f'{gen}/acados_ocp_solver_pyx.c') lenv2 = envCython.Clone() -lenv2["LINKFLAGS"] += [lib_solver[0].get_labspath()] +lenv2["LIBPATH"] += [lib_solver[0].dir.abspath] +lenv2["RPATH"] += [lenv2.Literal('\\$$ORIGIN')] lenv2.Command(libacados_ocp_solver_c, [acados_ocp_solver_pyx, acados_ocp_solver_common, libacados_ocp_solver_pxd], f'cython' + \ @@ -94,6 +97,6 @@ lenv2.Command(libacados_ocp_solver_c, f' -I {libacados_ocp_solver_pxd.get_dir().get_labspath()}' + \ f' -I {acados_ocp_solver_common.get_dir().get_labspath()}' + \ f' {acados_ocp_solver_pyx.get_labspath()}') -lib_cython = lenv2.Program(f'{gen}/acados_ocp_solver_pyx.so', [libacados_ocp_solver_c]) +lib_cython = lenv2.Program(f'{gen}/acados_ocp_solver_pyx.so', [libacados_ocp_solver_c], LIBS=['acados_ocp_solver_long']) lenv2.Depends(lib_cython, lib_solver) lenv2.Depends(libacados_ocp_solver_c, np_version) diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 75773dfeb1..bfdbce0d3b 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -4,7 +4,7 @@ import numpy as np import cereal.messaging as messaging from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.modeld.constants import ModelConstants @@ -21,7 +21,7 @@ LON_MPC_STEP = 0.2 # first step is 0.2s A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6] A_CRUISE_MAX_BP = [0., 10.0, 25., 40.] CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N] -ALLOW_THROTTLE_THRESHOLD = 0.5 +ALLOW_THROTTLE_THRESHOLD = 0.4 MIN_ALLOW_THROTTLE_SPEED = 2.5 # Lookup table for turns @@ -190,7 +190,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): def publish(self, sm, pm): plan_send = messaging.new_message('longitudinalPlan') - plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState']) + plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState', 'radarState']) longitudinalPlan = plan_send.longitudinalPlan longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2'] diff --git a/selfdrive/debug/car/fw_versions.py b/selfdrive/debug/car/fw_versions.py index 03d066cdcb..6ae10d2fb2 100755 --- a/selfdrive/debug/car/fw_versions.py +++ b/selfdrive/debug/car/fw_versions.py @@ -47,15 +47,15 @@ if __name__ == "__main__": num_pandas = len(messaging.recv_one_retry(pandaStates_sock).pandaStates) - t = time.time() + t = time.monotonic() print("Getting vin...") set_obd_multiplexing(True) vin_rx_addr, vin_rx_bus, vin = get_vin(*can_callbacks, (0, 1)) print(f'RX: {hex(vin_rx_addr)}, BUS: {vin_rx_bus}, VIN: {vin}') - print(f"Getting VIN took {time.time() - t:.3f} s") + print(f"Getting VIN took {time.monotonic() - t:.3f} s") print() - t = time.time() + t = time.monotonic() fw_vers = get_fw_versions(*can_callbacks, set_obd_multiplexing, query_brand=args.brand, extra=extra, num_pandas=num_pandas, progress=True) _, candidates = match_fw_to_car(fw_vers, vin) @@ -71,4 +71,4 @@ if __name__ == "__main__": print() print("Possible matches:", candidates) - print(f"Getting fw took {time.time() - t:.3f} s") + print(f"Getting fw took {time.monotonic() - t:.3f} s") diff --git a/selfdrive/debug/car/vw_mqb_config.py b/selfdrive/debug/car/vw_mqb_config.py index 7ff5774a71..3c55642e40 100755 --- a/selfdrive/debug/car/vw_mqb_config.py +++ b/selfdrive/debug/car/vw_mqb_config.py @@ -40,8 +40,7 @@ if __name__ == "__main__": panda = Panda() panda.set_safety_mode(CarParams.SafetyModel.elm327) - bus = 1 if panda.has_obd() else 0 - uds_client = UdsClient(panda, MQB_EPS_CAN_ADDR, MQB_EPS_CAN_ADDR + RX_OFFSET, bus, timeout=0.2) + uds_client = UdsClient(panda, MQB_EPS_CAN_ADDR, MQB_EPS_CAN_ADDR + RX_OFFSET, 1, timeout=0.2) try: uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC) diff --git a/selfdrive/debug/cpu_usage_stat.py b/selfdrive/debug/cpu_usage_stat.py index 234dfea3cc..397e9f35f5 100755 --- a/selfdrive/debug/cpu_usage_stat.py +++ b/selfdrive/debug/cpu_usage_stat.py @@ -37,8 +37,6 @@ monitored_proc_names = [ cpu_time_names = ['user', 'system', 'children_user', 'children_system'] -timer = getattr(time, 'monotonic', time.time) - def get_arg_parser(): parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) @@ -72,7 +70,7 @@ if __name__ == "__main__": print('Add monitored proc:', k) stats[k] = {'cpu_samples': defaultdict(list), 'min': defaultdict(lambda: None), 'max': defaultdict(lambda: None), 'avg': defaultdict(float), 'last_cpu_times': None, 'last_sys_time': None} - stats[k]['last_sys_time'] = timer() + stats[k]['last_sys_time'] = time.monotonic() stats[k]['last_cpu_times'] = p.cpu_times() monitored_procs.append(p) i = 0 @@ -80,7 +78,7 @@ if __name__ == "__main__": while True: for p in monitored_procs: k = ' '.join(p.cmdline()) - cur_sys_time = timer() + cur_sys_time = time.monotonic() cur_cpu_times = p.cpu_times() cpu_times = np.subtract(cur_cpu_times, stats[k]['last_cpu_times']) / (cur_sys_time - stats[k]['last_sys_time']) stats[k]['last_sys_time'] = cur_sys_time diff --git a/selfdrive/debug/run_process_on_route.py b/selfdrive/debug/run_process_on_route.py index 2ccb0fb3e7..d6743fedcb 100755 --- a/selfdrive/debug/run_process_on_route.py +++ b/selfdrive/debug/run_process_on_route.py @@ -1,23 +1,25 @@ #!/usr/bin/env python3 - import argparse from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, replay_process +from openpilot.selfdrive.test.process_replay.test_processes import EXCLUDED_PROCS from openpilot.tools.lib.logreader import LogReader, save_log +ALLOW_PROCS = {c.proc_name for c in CONFIGS} + if __name__ == "__main__": parser = argparse.ArgumentParser(description="Run process on route and create new logs", formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("--fingerprint", help="The fingerprint to use") parser.add_argument("route", help="The route name to use") - parser.add_argument("process", nargs='+', help="The process(s) to run") + parser.add_argument("--fingerprint", help="The fingerprint to use") + parser.add_argument("--whitelist-procs", nargs='*', default=ALLOW_PROCS, help="Whitelist given processes (e.g. controlsd)") + parser.add_argument("--blacklist-procs", nargs='*', default=EXCLUDED_PROCS, help="Blacklist given processes (e.g. controlsd)") args = parser.parse_args() - cfgs = [c for c in CONFIGS if c.proc_name in args.process] - - lr = LogReader(args.route) - inputs = list(lr) + allowed_procs = set(args.whitelist_procs) - set(args.blacklist_procs) + cfgs = [c for c in CONFIGS if c.proc_name in allowed_procs] + inputs = list(LogReader(args.route)) outputs = replay_process(cfgs, inputs, fingerprint=args.fingerprint) # Remove message generated by the process under test and merge in the new messages @@ -25,6 +27,6 @@ if __name__ == "__main__": inputs = [i for i in inputs if i.which() not in produces] outputs = sorted(inputs + outputs, key=lambda x: x.logMonoTime) - fn = f"{args.route.replace('/', '_')}_{'_'.join(args.process)}.zst" + fn = f"{args.route.replace('/', '_')}_{'_'.join(allowed_procs)}.zst" print(f"Saving log to {fn}") save_log(fn, outputs) diff --git a/selfdrive/locationd/calibrationd.py b/selfdrive/locationd/calibrationd.py index 59f30dbf77..03c044982e 100755 --- a/selfdrive/locationd/calibrationd.py +++ b/selfdrive/locationd/calibrationd.py @@ -14,7 +14,7 @@ from typing import NoReturn from cereal import log, car import cereal.messaging as messaging from openpilot.system.hardware import HARDWARE -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process from openpilot.common.transformations.orientation import rot_from_euler, euler_from_rot diff --git a/selfdrive/locationd/lagd.py b/selfdrive/locationd/lagd.py index d7834f7f1f..7f1d5fd032 100755 --- a/selfdrive/locationd/lagd.py +++ b/selfdrive/locationd/lagd.py @@ -12,6 +12,7 @@ from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose, fft_next_good_size, parabolic_peak_interp +from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle BLOCK_SIZE = 100 BLOCK_NUM = 50 @@ -374,6 +375,8 @@ def main(): lag, valid_blocks = initial_lag_params lag_learner.reset(lag, valid_blocks) + lagd_toggle = LagdToggle(CP) + while True: sm.update() if sm.all_checks(): @@ -392,3 +395,6 @@ def main(): if sm.frame % 1200 == 0: # cache every 60 seconds params.put_nonblocking("LiveDelay", lag_msg_dat) + + if sm.frame % 60 == 0: # read from and write to params every 3 seconds + lagd_toggle.update(lag_msg) diff --git a/selfdrive/locationd/models/car_kf.py b/selfdrive/locationd/models/car_kf.py index 349897f371..27cc4ef9c9 100755 --- a/selfdrive/locationd/models/car_kf.py +++ b/selfdrive/locationd/models/car_kf.py @@ -5,7 +5,7 @@ from typing import Any import numpy as np -from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY +from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.selfdrive.locationd.models.constants import ObservationKind from openpilot.common.swaglog import cloudlog @@ -93,8 +93,9 @@ class CarKalman(KalmanFilter): dim_state = CarKalman.initial_x.shape[0] name = CarKalman.name - # vehicle models comes from The Science of Vehicle Dynamics: Handling, Braking, and Ride of Road and Race Cars - # Model used is in 6.15 with formula from 6.198 + # Linearized single-track lateral dynamics, equations 7.211-7.213 + # Massimo Guiggiani, The Science of Vehicle Dynamics: Handling, Braking, and Ride of Road and Race Cars + # Springer Cham, 2023. doi: https://doi.org/10.1007/978-3-031-06461-6 # globals global_vars = [sp.Symbol(name) for name in CarKalman.global_vars] diff --git a/selfdrive/locationd/paramsd.py b/selfdrive/locationd/paramsd.py index ec15f501ae..b4084fe5bc 100755 --- a/selfdrive/locationd/paramsd.py +++ b/selfdrive/locationd/paramsd.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import os -import json import numpy as np import capnp @@ -207,12 +206,11 @@ def migrate_cached_vehicle_params_if_needed(params: Params): return try: - last_parameters_dict = json.loads(last_parameters_data_old) last_parameters_msg = messaging.new_message('liveParameters') last_parameters_msg.liveParameters.valid = True - last_parameters_msg.liveParameters.steerRatio = last_parameters_dict['steerRatio'] - last_parameters_msg.liveParameters.stiffnessFactor = last_parameters_dict['stiffnessFactor'] - last_parameters_msg.liveParameters.angleOffsetAverageDeg = last_parameters_dict['angleOffsetAverageDeg'] + last_parameters_msg.liveParameters.steerRatio = last_parameters_data_old['steerRatio'] + last_parameters_msg.liveParameters.stiffnessFactor = last_parameters_data_old['stiffnessFactor'] + last_parameters_msg.liveParameters.angleOffsetAverageDeg = last_parameters_data_old['angleOffsetAverageDeg'] params.put("LiveParametersV2", last_parameters_msg.to_bytes()) except Exception as e: cloudlog.error(f"Failed to perform parameter migration: {e}") diff --git a/selfdrive/locationd/test/test_paramsd.py b/selfdrive/locationd/test/test_paramsd.py index 2129bf4386..dd496b7675 100644 --- a/selfdrive/locationd/test/test_paramsd.py +++ b/selfdrive/locationd/test/test_paramsd.py @@ -1,6 +1,5 @@ import random import numpy as np -import json from cereal import messaging from openpilot.selfdrive.locationd.paramsd import retrieve_initial_vehicle_params, migrate_cached_vehicle_params_if_needed @@ -47,7 +46,7 @@ class TestParamsd: CP = next(m for m in lr if m.which() == "carParams").carParams msg = get_random_live_parameters(CP) - params.put("LiveParameters", json.dumps(msg.liveParameters.to_dict())) + params.put("LiveParameters", msg.liveParameters.to_dict()) params.put("CarParamsPrevRoute", CP.as_builder().to_bytes()) params.remove("LiveParametersV2") @@ -60,7 +59,7 @@ class TestParamsd: def test_read_saved_params_corrupted_old_format(self): params = Params() - params.put("LiveParameters", b'\x00\x00\x02\x00\x01\x00:F\xde\xed\xae;') + params.put("LiveParameters", {}) params.remove("LiveParametersV2") migrate_cached_vehicle_params_if_needed(params) diff --git a/selfdrive/locationd/torqued.py b/selfdrive/locationd/torqued.py index 0066708319..e0409957f5 100755 --- a/selfdrive/locationd/torqued.py +++ b/selfdrive/locationd/torqued.py @@ -4,14 +4,13 @@ from collections import deque, defaultdict import cereal.messaging as messaging from cereal import car, log -from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY +from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process, DT_MDL from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.locationd.helpers import PointBuckets, ParameterEstimator, PoseCalibrator, Pose - -from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle +from openpilot.sunnypilot.livedelay.helpers import get_lat_delay HISTORY = 5 # secs POINTS_PER_BUCKET = 1500 @@ -34,7 +33,7 @@ MIN_BUCKET_POINTS = np.array([100, 300, 500, 500, 500, 500, 300, 100]) MIN_ENGAGE_BUFFER = 2 # secs VERSION = 1 # bump this to invalidate old parameter caches -ALLOWED_CARS = ['toyota', 'hyundai', 'rivian'] +ALLOWED_CARS = ['toyota', 'hyundai', 'rivian', 'honda'] def slope2rot(slope): @@ -51,7 +50,7 @@ class TorqueBuckets(PointBuckets): break -class TorqueEstimator(ParameterEstimator, LagdToggle): +class TorqueEstimator(ParameterEstimator): def __init__(self, CP, decimated=False, track_all_points=False): super().__init__() self.CP = CP @@ -99,6 +98,7 @@ class TorqueEstimator(ParameterEstimator, LagdToggle): # try to restore cached params params = Params() + self.params = params params_cache = params.get("CarParamsPrevRoute") torque_cache = params.get("LiveTorqueParameters") if params_cache is not None and torque_cache is not None: @@ -180,7 +180,7 @@ class TorqueEstimator(ParameterEstimator, LagdToggle): elif which == "liveCalibration": self.calibrator.feed_live_calib(msg) elif which == "liveDelay": - self.lag = self.lagd_torqued_main(self.CP, msg) + self.lag = get_lat_delay(self.params, msg.lateralDelay) # calculate lateral accel from past steering torque elif which == "livePose": if len(self.raw_points['steer_torque']) == self.hist_len: diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 0da3c24e19..6802b45e6e 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -50,9 +50,9 @@ def tg_compile(flags, model_name): # Compile small models for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: flags = { - 'larch64': 'QCOM=1', - 'Darwin': 'CPU=1 IMAGE=0 JIT=2', - }.get(arch, 'LLVM=1 LLVMOPT=1 BEAM=0 IMAGE=0 JIT=2') + 'larch64': 'DEV=QCOM', + 'Darwin': 'DEV=CPU IMAGE=0', + }.get(arch, 'DEV=LLVM IMAGE=0') tg_compile(flags, model_name) # Compile BIG model if USB GPU is available @@ -62,7 +62,7 @@ if "USBGPU" in os.environ: devs = subprocess.check_output('python3 -c "from tinygrad import Device; print(list(Device.get_available_devices()))"', shell=True, cwd=env.Dir('#').abspath) if b"AMD" in devs: print("USB GPU detected... building") - flags = "AMD=1 AMD_IFACE=USB AMD_LLVM=1 NOLOCALS=0 IMAGE=0" + flags = "DEV=AMD AMD_IFACE=USB AMD_LLVM=1 NOLOCALS=0 IMAGE=0" bp = tg_compile(flags, "big_driving_policy") bv = tg_compile(flags, "big_driving_vision") lenv.SideEffect('lock', [bp, bv]) # tg doesn't support multi-process so build serially diff --git a/selfdrive/modeld/dmonitoringmodeld.py b/selfdrive/modeld/dmonitoringmodeld.py index 7ab31ed2b4..215056e6e6 100755 --- a/selfdrive/modeld/dmonitoringmodeld.py +++ b/selfdrive/modeld/dmonitoringmodeld.py @@ -1,13 +1,9 @@ #!/usr/bin/env python3 import os from openpilot.system.hardware import TICI +os.environ['DEV'] = 'QCOM' if TICI else 'LLVM' from tinygrad.tensor import Tensor from tinygrad.dtype import dtypes -if TICI: - from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address - os.environ['QCOM'] = '1' -else: - os.environ['LLVM'] = '1' import math import time import pickle @@ -24,6 +20,7 @@ from openpilot.common.transformations.model import dmonitoringmodel_intrinsics, from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.selfdrive.modeld.models.commonmodel_pyx import CLContext, MonitoringModelFrame from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid +from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address MODEL_WIDTH, MODEL_HEIGHT = DM_INPUT_SIZE CALIB_LEN = 3 @@ -93,7 +90,7 @@ class ModelState: self.tensor_inputs['input_img'] = Tensor(self.frame.buffer_from_cl(input_img_cl).reshape((1, MODEL_WIDTH*MODEL_HEIGHT)), dtype=dtypes.uint8).realize() - output = self.model_run(**self.tensor_inputs).numpy().flatten() + output = self.model_run(**self.tensor_inputs).contiguous().realize().uop.base.buffer.numpy() t2 = time.perf_counter() return output, t2 - t1 diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index 31bfd707bb..27bf4c6ebb 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -1,16 +1,11 @@ #!/usr/bin/env python3 import os from openpilot.system.hardware import TICI +os.environ['DEV'] = 'QCOM' if TICI else 'LLVM' USBGPU = "USBGPU" in os.environ if USBGPU: - os.environ['AMD'] = '1' + os.environ['DEV'] = 'AMD' os.environ['AMD_IFACE'] = 'USB' -elif TICI: - from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address - os.environ['QCOM'] = '1' -else: - os.environ['LLVM'] = '1' - os.environ['JIT'] = '2' from tinygrad.tensor import Tensor from tinygrad.dtype import dtypes import time @@ -34,8 +29,10 @@ from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext +from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address -from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle +from openpilot.sunnypilot.livedelay.helpers import get_lat_delay +from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase PROCESS_NAME = "selfdrive.modeld.modeld" @@ -83,13 +80,14 @@ class FrameMeta: if vipc is not None: self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof -class ModelState: +class ModelState(ModelStateBase): frames: dict[str, DrivingModelFrame] inputs: dict[str, np.ndarray] output: np.ndarray prev_desire: np.ndarray # for tracking the rising edge of the pulse def __init__(self, context: CLContext): + ModelStateBase.__init__(self) self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS with open(VISION_METADATA_PATH, 'rb') as f: vision_metadata = pickle.load(f) @@ -166,14 +164,14 @@ class ModelState: if prepare_only: return None - self.vision_output = self.vision_run(**self.vision_inputs).numpy().flatten() + self.vision_output = self.vision_run(**self.vision_inputs).contiguous().realize().uop.base.buffer.numpy() vision_outputs_dict = self.parser.parse_vision_outputs(self.slice_outputs(self.vision_output, self.vision_output_slices)) self.full_features_buffer[0,:-1] = self.full_features_buffer[0,1:] self.full_features_buffer[0,-1] = vision_outputs_dict['hidden_state'][0, :] self.numpy_inputs['features_buffer'][:] = self.full_features_buffer[0, self.temporal_idxs] - self.policy_output = self.policy_run(**self.policy_inputs).numpy().flatten() + self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy() policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices)) # TODO model only uses last value now @@ -253,8 +251,6 @@ def main(demo=False): CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) cloudlog.info("modeld got CarParams: %s", CP.brand) - modeld_lagd = LagdToggle() - # TODO this needs more thought, use .2s extra for now to estimate other delays # TODO Move smooth seconds to action function long_delay = CP.longitudinalActuatorDelay + LONG_SMOOTH_SECONDS @@ -300,7 +296,9 @@ def main(demo=False): is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId v_ego = max(sm["carState"].vEgo, 0.) - lat_delay = modeld_lagd.lagd_main(CP, sm, model) + if sm.frame % 60 == 0: + model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay) + lat_delay = model.lat_delay + LAT_SMOOTH_SECONDS lateral_control_params = np.array([v_ego, lat_delay], dtype=np.float32) if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) diff --git a/selfdrive/modeld/models/driving_policy.onnx b/selfdrive/modeld/models/driving_policy.onnx index 2fb716997d..187a3b2ecf 100644 --- a/selfdrive/modeld/models/driving_policy.onnx +++ b/selfdrive/modeld/models/driving_policy.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1741cad23f6f451782b5db6182218749ee12072e393d57eac36d8d5c55d9358a +oid sha256:22aec22a10ce09384d4a4af2a0bbff08d54af7e0c888503508f356fae4ff0e29 size 15583374 diff --git a/selfdrive/modeld/models/driving_vision.onnx b/selfdrive/modeld/models/driving_vision.onnx index fe636fb682..18f63358db 100644 --- a/selfdrive/modeld/models/driving_vision.onnx +++ b/selfdrive/modeld/models/driving_vision.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d2bd82ba42341dba1bda5426e45c4c646db604c9ac422156eaa2b9ef26194f9 +oid sha256:c824f68646a3b94f117f01c70dc8316fb466e05fbd42ccdba440b8a8dc86914b size 46265993 diff --git a/selfdrive/modeld/parse_model_outputs.py b/selfdrive/modeld/parse_model_outputs.py index 783572d436..b89de27594 100644 --- a/selfdrive/modeld/parse_model_outputs.py +++ b/selfdrive/modeld/parse_model_outputs.py @@ -90,17 +90,17 @@ class Parser: self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) - self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION, - out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) - for k in ['lead_prob', 'lane_lines_prob']: - self.parse_binary_crossentropy(k, outs) + self.parse_binary_crossentropy('lane_lines_prob', outs) self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH)) self.parse_binary_crossentropy('meta', outs) + self.parse_binary_crossentropy('lead_prob', outs) + self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION, + out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) return outs def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION, - out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) + out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) if 'lat_planner_solution' in outs: self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) if 'desired_curvature' in outs: diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 67776994f0..9649df9a2b 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -6,6 +6,7 @@ import cereal.messaging as messaging from openpilot.selfdrive.selfdrived.events import Events from openpilot.common.realtime import DT_DMON from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.common.params import Params from openpilot.common.stat_live import RunningStatFilter from openpilot.common.transformations.camera import DEVICE_CAMERAS @@ -159,6 +160,9 @@ class DriverMonitoring: self.threshold_pre = self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME self.threshold_prompt = self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME + self.params = Params() + self.too_distracted = self.params.get_bool("DriverTooDistracted") + self._reset_awareness() self._set_timers(active_monitoring=True) self._reset_events() @@ -305,10 +309,15 @@ class DriverMonitoring: def _update_events(self, driver_engaged, op_engaged, standstill, wrong_gear, car_speed): self._reset_events() - # Block engaging after max number of distrations or when alert active + # Block engaging until ignition cycle after max number or time of distractions if self.terminal_alert_cnt >= self.settings._MAX_TERMINAL_ALERTS or \ - self.terminal_time >= self.settings._MAX_TERMINAL_DURATION or \ - self.always_on and self.awareness <= self.threshold_prompt: + self.terminal_time >= self.settings._MAX_TERMINAL_DURATION: + if not self.too_distracted: + self.params.put_bool_nonblocking("DriverTooDistracted", True) + self.too_distracted = True + + # Always-on distraction lockout is temporary + if self.too_distracted or (self.always_on and self.awareness <= self.threshold_prompt): self.current_events.add(EventName.tooDistracted) always_on_valid = self.always_on and not wrong_gear diff --git a/selfdrive/pandad/can_list_to_can_capnp.cc b/selfdrive/pandad/can_list_to_can_capnp.cc index 6ad999da91..f2cf153453 100644 --- a/selfdrive/pandad/can_list_to_can_capnp.cc +++ b/selfdrive/pandad/can_list_to_can_capnp.cc @@ -1,5 +1,5 @@ #include "cereal/messaging/messaging.h" -#include "opendbc/can/common.h" +#include "selfdrive/pandad/can_types.h" void can_list_to_can_capnp_cpp(const std::vector &can_list, std::string &out, bool sendcan, bool valid) { MessageBuilder msg; diff --git a/selfdrive/pandad/can_types.h b/selfdrive/pandad/can_types.h new file mode 100644 index 0000000000..5fae581cfa --- /dev/null +++ b/selfdrive/pandad/can_types.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +struct CanFrame { + long src; + uint32_t address; + std::vector dat; +}; + +struct CanData { + uint64_t nanos; + std::vector frames; +}; \ No newline at end of file diff --git a/selfdrive/pandad/panda_safety.cc b/selfdrive/pandad/panda_safety.cc index 48589c4146..29e96032bf 100644 --- a/selfdrive/pandad/panda_safety.cc +++ b/selfdrive/pandad/panda_safety.cc @@ -2,9 +2,7 @@ #include "cereal/messaging/messaging.h" #include "common/swaglog.h" -void PandaSafety::configureSafetyMode() { - bool is_onroad = params_.getBool("IsOnroad"); - +void PandaSafety::configureSafetyMode(bool is_onroad) { if (is_onroad && !safety_configured_) { updateMultiplexingMode(); diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index af27276510..26f42add6c 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -36,8 +36,7 @@ // Ignition: // - If any of the ignition sources in any panda is high, ignition is high -#define MAX_IR_POWER 0.5f -#define MIN_IR_POWER 0.0f +#define MAX_IR_PANDA_VAL 50 #define CUTOFF_IL 400 #define SATURATE_IL 1000 @@ -85,6 +84,15 @@ Panda *connect(std::string serial="", uint32_t index=0) { panda->set_can_fd_auto(i, true); } + bool is_deprecated_panda = std::find(DEPRECATED_PANDA_TYPES.begin(), + DEPRECATED_PANDA_TYPES.end(), + panda->hw_type) != DEPRECATED_PANDA_TYPES.end(); + + if (is_deprecated_panda) { + LOGW("panda %s is deprecated (hw_type: %i), skipping firmware check...", panda->hw_serial().c_str(), static_cast(panda->hw_type)); + return panda.release(); + } + if (!panda->up_to_date() && !getenv("BOARDD_SKIP_FW_CHECK")) { throw std::runtime_error("Panda firmware out of date. Run pandad.py to update."); } @@ -169,7 +177,7 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda ps.setFanPower(health.fan_power); ps.setFanStallCount(health.fan_stall_count); ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid_pkt)); - ps.setSpiChecksumErrorCount(health.spi_checksum_error_count_pkt); + ps.setSpiErrorCount(health.spi_error_count_pkt); ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f); ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f); } @@ -202,7 +210,7 @@ void fill_panda_can_state(cereal::PandaState::PandaCanState::Builder &cs, const cs.setCanCoreResetCnt(can_health.can_core_reset_cnt); } -std::optional send_panda_states(PubMaster *pm, const std::vector &pandas, bool spoofing_started, bool always_offroad) { +std::optional send_panda_states(PubMaster *pm, const std::vector &pandas, bool is_onroad, bool spoofing_started, bool always_offroad) { bool ignition_local = false; const uint32_t pandas_cnt = pandas.size(); @@ -269,8 +277,9 @@ std::optional send_panda_states(PubMaster *pm, const std::vector panda->set_power_saving(power_save_desired); } - // set safety mode to NO_OUTPUT when car is off. ELM327 is an alternative if we want to leverage athenad/connect - if (!ignition_local && (health.safety_mode_pkt != (uint8_t)(cereal::CarParams::SafetyModel::NO_OUTPUT))) { + // set safety mode to NO_OUTPUT when car is off or we're not onroad. ELM327 is an alternative if we want to leverage athenad/connect + bool should_close_relay = !ignition_local || !is_onroad; + if (should_close_relay && (health.safety_mode_pkt != (uint8_t)(cereal::CarParams::SafetyModel::NO_OUTPUT))) { panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT); } @@ -337,14 +346,14 @@ void send_peripheral_state(Panda *panda, PubMaster *pm) { pm->send("peripheralState", msg); } -void process_panda_state(std::vector &pandas, PubMaster *pm, bool engaged, bool engaged_mads, bool spoofing_started, bool always_offroad) { +void process_panda_state(std::vector &pandas, PubMaster *pm, bool engaged, bool engaged_mads, bool is_onroad, bool spoofing_started, bool always_offroad) { std::vector connected_serials; for (Panda *p : pandas) { connected_serials.push_back(p->hw_serial()); } { - auto ignition_opt = send_panda_states(pm, pandas, spoofing_started, always_offroad); + auto ignition_opt = send_panda_states(pm, pandas, is_onroad, spoofing_started, always_offroad); if (!ignition_opt) { LOGE("Failed to get ignition_opt"); return; @@ -384,8 +393,8 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control) static uint64_t last_driver_camera_t = 0; static uint16_t prev_fan_speed = 999; - static uint16_t ir_pwr = 0; - static uint16_t prev_ir_pwr = 999; + static int ir_pwr = 0; + static int prev_ir_pwr = 999; static FirstOrderFilter integ_lines_filter(0, 30.0, 0.05); @@ -408,11 +417,11 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control) last_driver_camera_t = event.getLogMonoTime(); if (cur_integ_lines <= CUTOFF_IL) { - ir_pwr = 100.0 * MIN_IR_POWER; + ir_pwr = 0; } else if (cur_integ_lines > SATURATE_IL) { - ir_pwr = 100.0 * MAX_IR_POWER; + ir_pwr = 100; } else { - ir_pwr = 100.0 * (MIN_IR_POWER + ((cur_integ_lines - CUTOFF_IL) * (MAX_IR_POWER - MIN_IR_POWER) / (SATURATE_IL - CUTOFF_IL))); + ir_pwr = 100 * (cur_integ_lines - CUTOFF_IL) / (SATURATE_IL - CUTOFF_IL); } } @@ -421,8 +430,9 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control) ir_pwr = 0; } - if (ir_pwr != prev_ir_pwr || sm.frame % 100 == 0 || ir_pwr >= 50.0) { - panda->set_ir_pwr(ir_pwr); + if (ir_pwr != prev_ir_pwr || sm.frame % 100 == 0) { + int16_t ir_panda = util::map_val(ir_pwr, 0, 100, 0, MAX_IR_PANDA_VAL); + panda->set_ir_pwr(ir_panda); Hardware::set_ir_power(ir_pwr); prev_ir_pwr = ir_pwr; } @@ -437,6 +447,7 @@ void pandad_run(std::vector &pandas) { // Start the CAN send thread std::thread send_thread(can_send_thread, pandas, fake_send); + Params params; RateKeeper rk("pandad", 100); SubMaster sm({"selfdriveState", "selfdriveStateSP", "carParams"}); PubMaster pm({"can", "pandaStates", "peripheralState"}); @@ -444,6 +455,7 @@ void pandad_run(std::vector &pandas) { Panda *peripheral_panda = pandas[0]; bool engaged = false; bool engaged_mads = false; + bool is_onroad = false; bool always_offroad = false; // Main loop: receive CAN data and process states @@ -460,9 +472,10 @@ void pandad_run(std::vector &pandas) { sm.update(0); engaged = sm.allAliveAndValid({"selfdriveState"}) && sm["selfdriveState"].getSelfdriveState().getEnabled(); engaged_mads = process_mads_heartbeat(&sm); + is_onroad = params.getBool("IsOnroad"); always_offroad = panda_safety.getOffroadMode(); - process_panda_state(pandas, &pm, engaged, engaged_mads, spoofing_started, always_offroad); - panda_safety.configureSafetyMode(); + process_panda_state(pandas, &pm, engaged, engaged_mads, is_onroad, spoofing_started, always_offroad); + panda_safety.configureSafetyMode(is_onroad); } // Send out peripheralState at 2Hz @@ -487,7 +500,6 @@ void pandad_run(std::vector &pandas) { } // Close relay on exit to prevent a fault - const bool is_onroad = Params().getBool("IsOnroad"); if (is_onroad && !engaged) { for (auto &p : pandas) { if (p->connected()) { diff --git a/selfdrive/pandad/pandad.h b/selfdrive/pandad/pandad.h index 7b37fbe59a..cdacdc894b 100644 --- a/selfdrive/pandad/pandad.h +++ b/selfdrive/pandad/pandad.h @@ -8,10 +8,21 @@ void pandad_main_thread(std::vector serials); +// deprecated devices +static const std::vector DEPRECATED_PANDA_TYPES = { + cereal::PandaState::PandaType::WHITE_PANDA, + cereal::PandaState::PandaType::GREY_PANDA, + cereal::PandaState::PandaType::BLACK_PANDA, + cereal::PandaState::PandaType::PEDAL, + cereal::PandaState::PandaType::UNO, + cereal::PandaState::PandaType::RED_PANDA_V2 +}; + + class PandaSafety { public: PandaSafety(const std::vector &pandas) : pandas_(pandas) {} - void configureSafetyMode(); + void configureSafetyMode(bool is_onroad); bool getOffroadMode(); private: diff --git a/selfdrive/pandad/pandad.py b/selfdrive/pandad/pandad.py index b33ffb6473..9822a74f92 100755 --- a/selfdrive/pandad/pandad.py +++ b/selfdrive/pandad/pandad.py @@ -36,6 +36,12 @@ def flash_panda(panda_serial: str) -> Panda: panda_signature = b"" if panda.bootstub else panda.get_signature() cloudlog.warning(f"Panda {panda_serial} connected, version: {panda_version}, signature {panda_signature.hex()[:16]}, expected {fw_signature.hex()[:16]}") + # skip flashing if the detected device is deprecated from upstream + hw_type = panda.get_type() + if hw_type in Panda.DEPRECATED_DEVICES: + cloudlog.warning(f"Panda {panda_serial} is deprecated (hw_type: {hw_type}), skipping flash...") + return panda + if panda.bootstub or panda_signature != fw_signature: cloudlog.info("Panda firmware out of date, update required") panda.flash() diff --git a/selfdrive/pandad/pandad_api_impl.pyx b/selfdrive/pandad/pandad_api_impl.pyx index 6683b843ae..aaecb8a594 100644 --- a/selfdrive/pandad/pandad_api_impl.pyx +++ b/selfdrive/pandad/pandad_api_impl.pyx @@ -6,7 +6,7 @@ from libcpp.string cimport string from libcpp cimport bool from libc.stdint cimport uint8_t, uint32_t, uint64_t -cdef extern from "opendbc/can/common.h": +cdef extern from "selfdrive/pandad/can_types.h": cdef struct CanFrame: long src uint32_t address diff --git a/selfdrive/pandad/spi.cc b/selfdrive/pandad/spi.cc index 108b11b9dc..b6ee57801a 100644 --- a/selfdrive/pandad/spi.cc +++ b/selfdrive/pandad/spi.cc @@ -66,58 +66,44 @@ PandaSpiHandle::PandaSpiHandle(std::string serial) : PandaCommsHandle(serial) { // 50MHz is the max of the 845. note that some older // revs of the comma three may not support this speed uint32_t spi_speed = 50000000; - - if (!util::file_exists(SPI_DEVICE)) { - goto fail; - } - - spi_fd = open(SPI_DEVICE.c_str(), O_RDWR); - if (spi_fd < 0) { - LOGE("failed opening SPI device %d", spi_fd); - goto fail; - } - - // SPI settings - ret = util::safe_ioctl(spi_fd, SPI_IOC_WR_MODE, &spi_mode); - if (ret < 0) { - LOGE("failed setting SPI mode %d", ret); - goto fail; - } - - ret = util::safe_ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &spi_speed); - if (ret < 0) { - LOGE("failed setting SPI speed"); - goto fail; - } - - ret = util::safe_ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &spi_bits_per_word); - if (ret < 0) { - LOGE("failed setting SPI bits per word"); - goto fail; - } - - // get hw UID/serial - ret = control_read(0xc3, 0, 0, uid, uid_len, 100); - if (ret == uid_len) { - std::stringstream stream; - for (int i = 0; i < uid_len; i++) { - stream << std::hex << std::setw(2) << std::setfill('0') << int(uid[i]); + try { + if (!util::file_exists(SPI_DEVICE)) { + throw std::runtime_error("Error connecting to panda: SPI device not found"); } - hw_serial = stream.str(); - } else { - LOGD("failed to get serial %d", ret); - goto fail; - } - if (!serial.empty() && (serial != hw_serial)) { - goto fail; - } + spi_fd = open(SPI_DEVICE.c_str(), O_RDWR); + if (spi_fd < 0) { + LOGE("failed opening SPI device %d", spi_fd); + throw std::runtime_error("Error connecting to panda: failed to open SPI device"); + } + // SPI settings + util::safe_ioctl(spi_fd, SPI_IOC_WR_MODE, &spi_mode, "failed setting SPI mode"); + util::safe_ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &spi_speed, "failed setting SPI speed"); + util::safe_ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &spi_bits_per_word, "failed setting SPI bits per word"); + + // get hw UID/serial + ret = control_read(0xc3, 0, 0, uid, uid_len, 100); + if (ret == uid_len) { + std::stringstream stream; + for (int i = 0; i < uid_len; i++) { + stream << std::hex << std::setw(2) << std::setfill('0') << int(uid[i]); + } + hw_serial = stream.str(); + } else { + LOGD("failed to get serial %d", ret); + throw std::runtime_error("Error connecting to panda: failed to get serial"); + } + + if (!serial.empty() && (serial != hw_serial)) { + throw std::runtime_error("Error connecting to panda: serial mismatch"); + } + + } catch (...) { + cleanup(); + throw; + } return; - -fail: - cleanup(); - throw std::runtime_error("Error connecting to panda"); } PandaSpiHandle::~PandaSpiHandle() { diff --git a/selfdrive/selfdrived/alertmanager.py b/selfdrive/selfdrived/alertmanager.py index 79897404e4..86ea68809b 100644 --- a/selfdrive/selfdrived/alertmanager.py +++ b/selfdrive/selfdrived/alertmanager.py @@ -18,7 +18,7 @@ def set_offroad_alert(alert: str, show_alert: bool, extra_text: str = None) -> N if show_alert: a = copy.copy(OFFROAD_ALERTS[alert]) a['extra'] = extra_text or '' - Params().put(alert, json.dumps(a)) + Params().put(alert, a) else: Params().remove(alert) diff --git a/selfdrive/selfdrived/alerts_offroad.json b/selfdrive/selfdrived/alerts_offroad.json index 1668bf17fd..183c1f8547 100644 --- a/selfdrive/selfdrived/alerts_offroad.json +++ b/selfdrive/selfdrived/alerts_offroad.json @@ -25,18 +25,14 @@ "text": "An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install.", "severity": 0 }, - "Offroad_UnofficialHardware": { - "text": "Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support.", + "Offroad_UnregisteredHardware": { + "text": "Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support.", "severity": 1 }, "Offroad_StorageMissing": { "text": "NVMe drive not mounted.", "severity": 1 }, - "Offroad_BadNvme": { - "text": "Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe.", - "severity": 1 - }, "Offroad_CarUnrecognized": { "text": "sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai.", "severity": 0 @@ -48,5 +44,10 @@ "Offroad_OSMUpdateRequired": { "text": "OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.\n\n%1", "severity": 0 + }, + "Offroad_ExcessiveActuation": { + "text": "openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.", + "severity": 1, + "_comment": "Set extra field to lateral or longitudinal." } } diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index 9bfc638db3..591e54918e 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -4,10 +4,12 @@ import os from cereal import log, car import cereal.messaging as messaging -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.git import get_short_branch from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER +from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER +from openpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ NoEntryAlert, SoftDisableAlert, UserSoftDisableAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, \ @@ -40,6 +42,7 @@ class Events(EventsBase): return log.OnroadEvent + # ********** helper functions ********** def get_display_speed(speed_ms: float, metric: bool) -> str: speed = int(round(speed_ms * (CV.MS_TO_KPH if metric else CV.MS_TO_MPH))) @@ -92,6 +95,14 @@ def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messag Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2) +def audio_feedback_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + duration = FEEDBACK_MAX_DURATION - ((sm['audioFeedback'].blockNum + 1) * SAMPLE_BUFFER / SAMPLE_RATE) + return NormalPermanentAlert( + "Recording Audio Feedback", + f"{round(duration)} second{'s' if round(duration) != 1 else ''} remaining. Press again to save early.", + priority=Priority.LOW) + + # *** debug alerts *** def out_of_space_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: @@ -203,6 +214,7 @@ def invalid_lkas_setting_alert(CP: car.CarParams, CS: car.CarState, sm: messagin return NormalPermanentAlert("Invalid LKAS setting", text) + EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { # ********** events with no alerts ********** @@ -591,6 +603,11 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { ET.NO_ENTRY: NoEntryAlert("Distraction Level Too High"), }, + EventName.excessiveActuation: { + ET.SOFT_DISABLE: soft_disable_alert("Excessive Actuation"), + ET.NO_ENTRY: NoEntryAlert("Excessive Actuation"), + }, + EventName.overheat: { ET.PERMANENT: overheat_alert, ET.SOFT_DISABLE: soft_disable_alert("System Overheated"), @@ -819,9 +836,13 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { ET.WARNING: personality_changed_alert, }, - EventName.userFlag: { + EventName.userBookmark: { ET.PERMANENT: NormalPermanentAlert("Bookmark Saved", duration=1.5), }, + + EventName.audioFeedback: { + ET.PERMANENT: audio_feedback_alert, + }, } diff --git a/selfdrive/selfdrived/helpers.py b/selfdrive/selfdrived/helpers.py new file mode 100644 index 0000000000..f7468cbe43 --- /dev/null +++ b/selfdrive/selfdrived/helpers.py @@ -0,0 +1,54 @@ +import math +from enum import StrEnum, auto + +from cereal import car, messaging +from openpilot.common.realtime import DT_CTRL +from openpilot.selfdrive.locationd.helpers import Pose +from opendbc.car import ACCELERATION_DUE_TO_GRAVITY +from opendbc.car.lateral import ISO_LATERAL_ACCEL +from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX + +MIN_EXCESSIVE_ACTUATION_COUNT = int(0.25 / DT_CTRL) +MIN_LATERAL_ENGAGE_BUFFER = int(1 / DT_CTRL) + + +class ExcessiveActuationType(StrEnum): + LONGITUDINAL = auto() + LATERAL = auto() + + +class ExcessiveActuationCheck: + def __init__(self): + self._excessive_counter = 0 + self._engaged_counter = 0 + + def update(self, sm: messaging.SubMaster, CS: car.CarState, calibrated_pose: Pose) -> ExcessiveActuationType | None: + # CS.aEgo can be noisy to bumps in the road, transitioning from standstill, losing traction, etc. + # longitudinal + accel_calibrated = calibrated_pose.acceleration.x + excessive_long_actuation = sm['carControl'].longActive and (accel_calibrated > ACCEL_MAX * 2 or accel_calibrated < ACCEL_MIN * 2) + + # lateral + yaw_rate = calibrated_pose.angular_velocity.yaw + roll = sm['liveParameters'].roll + roll_compensated_lateral_accel = (CS.vEgo * yaw_rate) - (math.sin(roll) * ACCELERATION_DUE_TO_GRAVITY) + + # Prevent false positives after overriding + excessive_lat_actuation = False + self._engaged_counter = self._engaged_counter + 1 if sm['carControl'].latActive and not CS.steeringPressed else 0 + if self._engaged_counter > MIN_LATERAL_ENGAGE_BUFFER: + if abs(roll_compensated_lateral_accel) > ISO_LATERAL_ACCEL * 2: + excessive_lat_actuation = True + + # livePose acceleration can be noisy due to bad mounting or aliased livePose measurements + livepose_valid = abs(CS.aEgo - accel_calibrated) < 2 + self._excessive_counter = self._excessive_counter + 1 if livepose_valid and (excessive_long_actuation or excessive_lat_actuation) else 0 + + excessive_type = None + if self._excessive_counter > MIN_EXCESSIVE_ACTUATION_COUNT: + if excessive_long_actuation: + excessive_type = ExcessiveActuationType.LONGITUDINAL + else: + excessive_type = ExcessiveActuationType.LATERAL + + return excessive_type diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 07914f90da..342c78b8d3 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -15,7 +15,9 @@ from openpilot.common.swaglog import cloudlog from openpilot.common.gps import get_gps_location_service from openpilot.selfdrive.car.car_specific import CarSpecificEvents +from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose from openpilot.selfdrive.selfdrived.events import Events, ET +from openpilot.selfdrive.selfdrived.helpers import ExcessiveActuationCheck from openpilot.selfdrive.selfdrived.state import StateMachine from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroad_alert @@ -30,6 +32,7 @@ from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP REPLAY = "REPLAY" in os.environ SIMULATION = "SIMULATION" in os.environ TESTING_CLOSET = "TESTING_CLOSET" in os.environ + LONGITUDINAL_PERSONALITY_MAP = {v: k for k, v in log.LongitudinalPersonality.schema.enumerants.items()} ThermalStatus = log.DeviceState.ThermalStatus @@ -67,6 +70,11 @@ class SelfdriveD(CruiseHelper): self.car_events = CarSpecificEvents(self.CP) + self.pose_calibrator = PoseCalibrator() + self.calibrated_pose: Pose | None = None + self.excessive_actuation_check = ExcessiveActuationCheck() + self.excessive_actuation = self.params.get("Offroad_ExcessiveActuation") is not None + # Setup sockets self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents'] + ['selfdriveStateSP', 'onroadEventsSP']) @@ -87,7 +95,7 @@ class SelfdriveD(CruiseHelper): self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration', 'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'liveDelay', 'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters', - 'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userFlag'] + \ + 'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback'] + \ self.camera_packets + self.sensor_packets + self.gps_packets, ignore_alive=ignore, ignore_avg_freq=ignore, ignore_valid=ignore, frequency=int(1/DT_CTRL)) @@ -121,7 +129,7 @@ class SelfdriveD(CruiseHelper): self.logged_comm_issue = None self.not_running_prev = None self.experimental_mode = False - self.personality = self.read_personality_param() + self.personality = self.params.get("LongitudinalPersonality", return_default=True) self.recalibrating_seen = False self.state_machine = StateMachine() self.rk = Ratekeeper(100, print_delay_threshold=None) @@ -183,9 +191,12 @@ class SelfdriveD(CruiseHelper): self.events.add(EventName.selfdriveInitializing) return - # Check for user flag (bookmark) press - if self.sm.updated['userFlag']: - self.events.add(EventName.userFlag) + # Check for user bookmark press (bookmark button or end of LKAS button feedback) + if self.sm.updated['userBookmark']: + self.events.add(EventName.userBookmark) + + if self.sm.updated['audioFeedback']: + self.events.add(EventName.audioFeedback) # Don't add any more events while in dashcam mode if self.CP.passive: @@ -254,6 +265,22 @@ class SelfdriveD(CruiseHelper): if self.sm['driverAssistance'].leftLaneDeparture or self.sm['driverAssistance'].rightLaneDeparture: self.events.add(EventName.ldw) + # Check for excessive actuation + if self.sm.updated['liveCalibration']: + self.pose_calibrator.feed_live_calib(self.sm['liveCalibration']) + if self.sm.updated['livePose']: + device_pose = Pose.from_live_pose(self.sm['livePose']) + self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_pose) + + if self.calibrated_pose is not None: + excessive_actuation = self.excessive_actuation_check.update(self.sm, CS, self.calibrated_pose) + if not self.excessive_actuation and excessive_actuation is not None: + set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text=str(excessive_actuation)) + self.excessive_actuation = True + + if self.excessive_actuation: + self.events.add(EventName.excessiveActuation) + # Handle lane change if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange: direction = self.sm['modelV2'].meta.laneChangeDirection @@ -305,13 +332,12 @@ class SelfdriveD(CruiseHelper): self.events.add(EventName.cameraFrameRate) if not REPLAY and self.rk.lagging: self.events.add(EventName.selfdrivedLagging) - if not self.sm.valid['radarState']: - if self.sm['radarState'].radarErrors.canError: - self.events.add(EventName.canError) - elif self.sm['radarState'].radarErrors.radarUnavailableTemporary: - self.events.add(EventName.radarTempUnavailable) - else: - self.events.add(EventName.radarFault) + if self.sm['radarState'].radarErrors.canError: + self.events.add(EventName.canError) + elif self.sm['radarState'].radarErrors.radarUnavailableTemporary: + self.events.add(EventName.radarTempUnavailable) + elif any(self.sm['radarState'].radarErrors.to_dict().values()): + self.events.add(EventName.radarFault) if not self.sm.valid['pandaStates']: self.events.add(EventName.usbError) if CS.canTimeout: @@ -407,13 +433,13 @@ class SelfdriveD(CruiseHelper): if any(not be.pressed and be.type == ButtonType.gapAdjustCruise for be in CS.buttonEvents): if not self.experimental_mode_switched: self.personality = (self.personality - 1) % 3 - self.params.put_nonblocking('LongitudinalPersonality', str(self.personality)) + self.params.put_nonblocking('LongitudinalPersonality', self.personality) self.events.add(EventName.personalityChanged) self.experimental_mode_switched = False def data_sample(self): - car_state = messaging.recv_one(self.car_state_sock) - CS = car_state.carState if car_state else self.CS_prev + _car_state = messaging.recv_one(self.car_state_sock) + CS = _car_state.carState if _car_state else self.CS_prev self.sm.update(0) @@ -538,19 +564,13 @@ class SelfdriveD(CruiseHelper): self.CS_prev = CS - def read_personality_param(self): - try: - return int(self.params.get('LongitudinalPersonality')) - except (ValueError, TypeError): - return log.LongitudinalPersonality.standard - def params_thread(self, evt): while not evt.is_set(): self.is_metric = self.params.get_bool("IsMetric") self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled") self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator") self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl - self.personality = self.read_personality_param() + self.personality = self.params.get("LongitudinalPersonality", return_default=True) self.mads.read_params() time.sleep(0.1) diff --git a/selfdrive/selfdrived/tests/test_alerts.py b/selfdrive/selfdrived/tests/test_alerts.py index 75365b7a52..a1867f2289 100644 --- a/selfdrive/selfdrived/tests/test_alerts.py +++ b/selfdrive/selfdrived/tests/test_alerts.py @@ -111,7 +111,7 @@ class TestAlerts: alert = copy.copy(self.offroad_alerts[a]) set_offroad_alert(a, True) alert['extra'] = '' - assert json.dumps(alert) == params.get(a, encoding='utf8') + assert alert == params.get(a) # then delete it set_offroad_alert(a, False) @@ -125,6 +125,6 @@ class TestAlerts: alert = self.offroad_alerts[a] set_offroad_alert(a, True, extra_text="a"*i) - written_alert = json.loads(params.get(a, encoding='utf8')) + written_alert = params.get(a) assert "a"*i == written_alert['extra'] assert alert["text"] == written_alert['text'] diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index bc5ccf7845..b1190c25e9 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -4,8 +4,9 @@ import time import copy import heapq import signal -from collections import Counter, OrderedDict +from collections import Counter from dataclasses import dataclass, field +from itertools import islice from typing import Any from collections.abc import Callable, Iterable from tqdm import tqdm @@ -16,12 +17,13 @@ import cereal.messaging as messaging from cereal import car from cereal.services import SERVICE_LIST from msgq.visionipc import VisionIpcServer, get_endpoint_name as vipc_get_endpoint_name +from opendbc.car.can_definitions import CanData from opendbc.car.car_helpers import get_car, interfaces from openpilot.common.params import Params from openpilot.common.prefix import OpenpilotPrefix from openpilot.common.timeout import Timeout from openpilot.common.realtime import DT_CTRL -from openpilot.selfdrive.car.card import can_comm_callbacks, convert_to_capnp +from openpilot.selfdrive.car.card import convert_to_capnp from openpilot.system.manager.process_config import managed_processes from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state, available_streams from openpilot.selfdrive.test.process_replay.migration import migrate_all @@ -30,22 +32,10 @@ from openpilot.tools.lib.logreader import LogIterable from openpilot.tools.lib.framereader import FrameReader # Numpy gives different results based on CPU features after version 19 -NUMPY_TOLERANCE = 1e-7 +NUMPY_TOLERANCE = 1e-2 PROC_REPLAY_DIR = os.path.dirname(os.path.abspath(__file__)) FAKEDATA = os.path.join(PROC_REPLAY_DIR, "fakedata/") -class DummySocket: - def __init__(self): - self.data: list[bytes] = [] - - def receive(self, non_blocking: bool = False) -> bytes | None: - if non_blocking: - return None - - return self.data.pop() - - def send(self, data: bytes): - self.data.append(data) class LauncherWithCapture: def __init__(self, capture: ProcessOutputCapture, launcher: Callable): @@ -63,8 +53,7 @@ class ReplayContext: self.pubs = cfg.pubs self.main_pub = cfg.main_pub self.main_pub_drained = cfg.main_pub_drained - self.unlocked_pubs = cfg.unlocked_pubs - assert(len(self.pubs) != 0 or self.main_pub is not None) + assert len(self.pubs) != 0 or self.main_pub is not None def __enter__(self): self.open_context() @@ -79,9 +68,8 @@ class ReplayContext: messaging.set_fake_prefix(self.proc_name) if self.main_pub is None: - self.events = OrderedDict() - pubs_with_events = [pub for pub in self.pubs if pub not in self.unlocked_pubs] - for pub in pubs_with_events: + self.events = {} + for pub in self.pubs: self.events[pub] = messaging.fake_event_handle(pub, enable=True) else: self.events = {self.main_pub: messaging.fake_event_handle(self.main_pub, enable=True)} @@ -138,16 +126,21 @@ class ProcessConfig: processing_time: float = 0.001 timeout: int = 30 simulation: bool = True + # Set to service process receives on first main_pub: str | None = None - main_pub_drained: bool = True + main_pub_drained: bool = False vision_pubs: list[str] = field(default_factory=list) ignore_alive_pubs: list[str] = field(default_factory=list) - unlocked_pubs: list[str] = field(default_factory=list) + + def __post_init__(self): + # If the process is polling a service, we can just lock that one to speed up replay + if self.main_pub is None and isinstance(self.should_recv_callback, MessageBasedRcvCallback): + self.main_pub = self.should_recv_callback.trigger_msg_type class ProcessContainer: def __init__(self, cfg: ProcessConfig): - self.prefix = OpenpilotPrefix(clean_dirs_on_exit=False) + self.prefix = OpenpilotPrefix(create_dirs_on_enter=False, clean_dirs_on_exit=False) self.cfg = copy.deepcopy(cfg) self.process = copy.deepcopy(managed_processes[cfg.proc_name]) self.msg_queue: list[capnp._DynamicStructReader] = [] @@ -229,6 +222,7 @@ class ProcessContainer: fingerprint: str | None, capture_output: bool ): with self.prefix as p: + self.prefix.create_dirs() self._setup_env(params_config, environ_config) if self.cfg.config_callback is not None: @@ -254,11 +248,6 @@ class ProcessContainer: if self.cfg.init_callback is not None: self.cfg.init_callback(self.rc, self.pm, all_msgs, fingerprint) - # wait for process to startup - with Timeout(10, error_msg=f"timed out waiting for process to start: {repr(self.cfg.proc_name)}"): - while not all(self.pm.all_readers_updated(s) for s in self.cfg.pubs if s not in self.cfg.ignore_alive_pubs): - time.sleep(0) - def stop(self): with self.prefix: self.process.signal(signal.SIGKILL) @@ -267,28 +256,42 @@ class ProcessContainer: self.prefix.clean_dirs() self._clean_env() + def get_output_msgs(self, start_time: int): + assert self.rc and self.sockets + + output_msgs = [] + self.rc.wait_for_recv_called() + for socket in self.sockets: + ms = messaging.drain_sock(socket) + for m in ms: + m = m.as_builder() + m.logMonoTime = start_time + int(self.cfg.processing_time * 1e9) + output_msgs.append(m.as_reader()) + return output_msgs + def run_step(self, msg: capnp._DynamicStructReader, frs: dict[str, FrameReader] | None) -> list[capnp._DynamicStructReader]: assert self.rc and self.pm and self.sockets and self.process.proc output_msgs = [] - with self.prefix, Timeout(self.cfg.timeout, error_msg=f"timed out testing process {repr(self.cfg.proc_name)}"): - end_of_cycle = True - if self.cfg.should_recv_callback is not None: - end_of_cycle = self.cfg.should_recv_callback(msg, self.cfg, self.cnt) - - self.msg_queue.append(msg) - if end_of_cycle: - self.rc.wait_for_recv_called() + end_of_cycle = True + if self.cfg.should_recv_callback is not None: + end_of_cycle = self.cfg.should_recv_callback(msg, self.cfg, self.cnt) + self.msg_queue.append(msg) + if end_of_cycle: + with self.prefix, Timeout(self.cfg.timeout, error_msg=f"timed out testing process {repr(self.cfg.proc_name)}"): # call recv to let sub-sockets reconnect, after we know the process is ready if self.cnt == 0: for s in self.sockets: messaging.recv_one_or_none(s) - # empty recv on drained pub indicates the end of messages, only do that if there're any + # certain processes use drain_sock. need to cause empty recv to break from this loop trigger_empty_recv = False if self.cfg.main_pub and self.cfg.main_pub_drained: - trigger_empty_recv = next((True for m in self.msg_queue if m.which() == self.cfg.main_pub), False) + trigger_empty_recv = any(m.which() == self.cfg.main_pub for m in self.msg_queue) + + # get output msgs from previous inputs + output_msgs = self.get_output_msgs(msg.logMonoTime) for m in self.msg_queue: self.pm.send(m.which(), m.as_builder()) @@ -303,14 +306,8 @@ class ProcessContainer: self.msg_queue = [] self.rc.unlock_sockets() - self.rc.wait_for_next_recv(trigger_empty_recv) - - for socket in self.sockets: - ms = messaging.drain_sock(socket) - for m in ms: - m = m.as_builder() - m.logMonoTime = msg.logMonoTime + int(self.cfg.processing_time * 1e9) - output_msgs.append(m.as_reader()) + if trigger_empty_recv: + self.rc.unlock_sockets() self.cnt += 1 assert self.process.proc.is_alive() @@ -320,7 +317,7 @@ class ProcessContainer: def card_fingerprint_callback(rc, pm, msgs, fingerprint): print("start fingerprinting") params = Params() - canmsgs = [msg for msg in msgs if msg.which() == "can"][:300] + canmsgs = list(islice((m for m in msgs if m.which() == "can"), 300)) # card expects one arbitrary can and pandaState rc.send_sync(pm, "can", messaging.new_message("can", 1)) @@ -345,36 +342,27 @@ def get_car_params_callback(rc, pm, msgs, fingerprint): CP = CarInterface.get_non_essential_params(fingerprint) CP_SP = CarInterface.get_non_essential_params_sp(CP, fingerprint) else: - can = DummySocket() - sendcan = DummySocket() - - canmsgs = [msg for msg in msgs if msg.which() == "can"] + can_msgs = ([CanData(can.address, can.dat, can.src) for can in m.can] for m in msgs if m.which() == "can") cached_params_raw = params.get("CarParamsCache") - has_cached_cp = cached_params_raw is not None - assert len(canmsgs) != 0, "CAN messages are required for fingerprinting" - assert os.environ.get("SKIP_FW_QUERY", False) or has_cached_cp, \ + assert next(can_msgs, None), "CAN messages are required for fingerprinting" + assert os.environ.get("SKIP_FW_QUERY", False) or cached_params_raw is not None, \ "CarParamsCache is required for fingerprinting. Make sure to keep carParams msgs in the logs." - for m in canmsgs[:300]: - can.send(m.as_builder().to_bytes()) - can_callbacks = can_comm_callbacks(can, sendcan) + def can_recv(wait_for_one: bool = False) -> list[list[CanData]]: + return [next(can_msgs, [])] cached_params = None - if has_cached_cp: + if cached_params_raw is not None: with car.CarParams.from_bytes(cached_params_raw) as _cached_params: cached_params = _cached_params - _CI = get_car(*can_callbacks, lambda obd: None, Params().get_bool("AlphaLongitudinalEnabled"), False, cached_params=cached_params) + _CI = get_car(can_recv, lambda _msgs: None, lambda obd: None, params.get_bool("AlphaLongitudinalEnabled"), False, cached_params=cached_params) CP, CP_SP = _CI.CP, _CI.CP_SP params.put("CarParams", CP.to_bytes()) params.put("CarParamsSP", convert_to_capnp(CP_SP).to_bytes()) -def selfdrived_rcv_callback(msg, cfg, frame): - return (frame - 1) == 0 or msg.which() == 'carState' - - def card_rcv_callback(msg, cfg, frame): # no sendcan until card is initialized if msg.which() != "can": @@ -389,21 +377,6 @@ def card_rcv_callback(msg, cfg, frame): return len(socks) > 0 -def calibration_rcv_callback(msg, cfg, frame): - # calibrationd publishes 1 calibrationData every 5 cameraOdometry packets. - # should_recv always true to increment frame - return (frame - 1) == 0 or msg.which() == 'cameraOdometry' - - -def torqued_rcv_callback(msg, cfg, frame): - # should_recv always true to increment frame - return (frame - 1) == 0 or msg.which() == 'livePose' - - -def dmonitoringmodeld_rcv_callback(msg, cfg, frame): - return msg.which() == "driverCameraState" - - class ModeldCameraSyncRcvCallback: def __init__(self): self.road_present = False @@ -428,26 +401,13 @@ class ModeldCameraSyncRcvCallback: class MessageBasedRcvCallback: - def __init__(self, trigger_msg_type): + def __init__(self, trigger_msg_type: str, first_frame: bool = False): self.trigger_msg_type = trigger_msg_type + self.first_frame = first_frame def __call__(self, msg, cfg, frame): - return msg.which() == self.trigger_msg_type - - -class FrequencyBasedRcvCallback: - def __init__(self, trigger_msg_type): - self.trigger_msg_type = trigger_msg_type - - def __call__(self, msg, cfg, frame): - if msg.which() != self.trigger_msg_type: - return False - - resp_sockets = [ - s for s in cfg.subs - if frame % max(1, int(SERVICE_LIST[msg.which()].frequency / SERVICE_LIST[s].frequency)) == 0 - ] - return bool(len(resp_sockets)) + # publish on first frame or trigger msg + return ((frame - 1) == 0 and self.first_frame) or msg.which() == self.trigger_msg_type def selfdrived_config_callback(params, cfg, lr): @@ -462,16 +422,16 @@ CONFIGS = [ proc_name="selfdrived", pubs=[ "carState", "deviceState", "pandaStates", "peripheralState", "liveCalibration", "driverMonitoringState", - "longitudinalPlan", "livePose", "liveDelay", "liveParameters", "radarState", - "modelV2", "driverCameraState", "roadCameraState", "wideRoadCameraState", "managerState", - "liveTorqueParameters", "accelerometer", "gyroscope", "carOutput", - "gpsLocationExternal", "gpsLocation", "controlsState", "carControl", "driverAssistance", "alertDebug", + "longitudinalPlan", "livePose", "liveDelay", "liveParameters", "radarState", "modelV2", + "driverCameraState", "roadCameraState", "wideRoadCameraState", "managerState", "liveTorqueParameters", + "accelerometer", "gyroscope", "carOutput", "gpsLocationExternal", "gpsLocation", "controlsState", + "carControl", "driverAssistance", "alertDebug", "audioFeedback", ], subs=["selfdriveState", "onroadEvents"], ignore=["logMonoTime"], config_callback=selfdrived_config_callback, init_callback=get_car_params_callback, - should_recv_callback=selfdrived_rcv_callback, + should_recv_callback=MessageBasedRcvCallback("carState", True), tolerance=NUMPY_TOLERANCE, processing_time=0.004, ), @@ -496,6 +456,7 @@ CONFIGS = [ tolerance=NUMPY_TOLERANCE, processing_time=0.004, main_pub="can", + main_pub_drained=True, ), ProcessConfig( proc_name="radard", @@ -503,7 +464,7 @@ CONFIGS = [ subs=["radarState"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=FrequencyBasedRcvCallback("modelV2"), + should_recv_callback=MessageBasedRcvCallback("modelV2"), ), ProcessConfig( proc_name="plannerd", @@ -511,7 +472,7 @@ CONFIGS = [ subs=["longitudinalPlan", "driverAssistance"], ignore=["logMonoTime", "longitudinalPlan.processingDelay", "longitudinalPlan.solverExecutionTime"], init_callback=get_car_params_callback, - should_recv_callback=FrequencyBasedRcvCallback("modelV2"), + should_recv_callback=MessageBasedRcvCallback("modelV2"), tolerance=NUMPY_TOLERANCE, ), ProcessConfig( @@ -520,14 +481,14 @@ CONFIGS = [ subs=["liveCalibration"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=calibration_rcv_callback, + should_recv_callback=MessageBasedRcvCallback("cameraOdometry", True), ), ProcessConfig( proc_name="dmonitoringd", pubs=["driverStateV2", "liveCalibration", "carState", "modelV2", "selfdriveState"], subs=["driverMonitoringState"], ignore=["logMonoTime"], - should_recv_callback=FrequencyBasedRcvCallback("driverStateV2"), + should_recv_callback=MessageBasedRcvCallback("driverStateV2"), tolerance=NUMPY_TOLERANCE, ), ProcessConfig( @@ -539,7 +500,6 @@ CONFIGS = [ ignore=["logMonoTime"], should_recv_callback=MessageBasedRcvCallback("cameraOdometry"), tolerance=NUMPY_TOLERANCE, - unlocked_pubs=["accelerometer", "gyroscope"], ), ProcessConfig( proc_name="paramsd", @@ -547,7 +507,7 @@ CONFIGS = [ subs=["liveParameters"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=FrequencyBasedRcvCallback("livePose"), + should_recv_callback=MessageBasedRcvCallback("livePose"), tolerance=NUMPY_TOLERANCE, processing_time=0.004, ), @@ -572,7 +532,7 @@ CONFIGS = [ subs=["liveTorqueParameters"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=torqued_rcv_callback, + should_recv_callback=MessageBasedRcvCallback("livePose", True), tolerance=NUMPY_TOLERANCE, ), ProcessConfig( @@ -584,7 +544,6 @@ CONFIGS = [ tolerance=NUMPY_TOLERANCE, processing_time=0.020, main_pub=vipc_get_endpoint_name("camerad", meta_from_camera_state("roadCameraState").stream), - main_pub_drained=False, vision_pubs=["roadCameraState", "wideRoadCameraState"], ignore_alive_pubs=["wideRoadCameraState"], init_callback=get_car_params_callback, @@ -594,11 +553,10 @@ CONFIGS = [ pubs=["liveCalibration", "driverCameraState"], subs=["driverStateV2"], ignore=["logMonoTime", "driverStateV2.modelExecutionTime", "driverStateV2.gpuExecutionTime"], - should_recv_callback=dmonitoringmodeld_rcv_callback, + should_recv_callback=MessageBasedRcvCallback("driverCameraState"), tolerance=NUMPY_TOLERANCE, processing_time=0.020, main_pub=vipc_get_endpoint_name("camerad", meta_from_camera_state("driverCameraState").stream), - main_pub_drained=False, vision_pubs=["driverCameraState"], ignore_alive_pubs=["driverCameraState"], ), @@ -706,8 +664,8 @@ def _replay_multi_process( all_msgs = sorted(lr, key=lambda msg: msg.logMonoTime) log_msgs = [] + containers = [] try: - containers = [] for cfg in cfgs: container = ProcessContainer(cfg) containers.append(container) @@ -742,6 +700,11 @@ def _replay_multi_process( internal_pub_queue.append(m) heapq.heappush(internal_pub_index_heap, (m.logMonoTime, len(internal_pub_queue) - 1)) log_msgs.extend(output_msgs) + + # flush last set of messages from each process + for container in containers: + last_time = log_msgs[-1].logMonoTime if len(log_msgs) > 0 else int(time.monotonic() * 1e9) + log_msgs.extend(container.get_output_msgs(last_time)) finally: for container in containers: container.stop() diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 79c03c65a8..43f97e212c 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -de16c6fbe14e121c5e74cd944ce03a0e4672540d \ No newline at end of file +62b3e8590fe85f87494517b3177a0ccaad1e17e5 \ No newline at end of file diff --git a/selfdrive/test/setup_device_ci.sh b/selfdrive/test/setup_device_ci.sh index d717698514..98909bfb52 100755 --- a/selfdrive/test/setup_device_ci.sh +++ b/selfdrive/test/setup_device_ci.sh @@ -54,6 +54,7 @@ while true; do # /data/ciui.py & #fi + awk '{print \$1}' /proc/uptime > /var/tmp/power_watchdog sleep 5s done diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index f98e3bd88c..935da99c10 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -55,6 +55,7 @@ PROCS = { "selfdrive.locationd.paramsd": 9.0, "selfdrive.locationd.lagd": 11.0, "selfdrive.ui.soundd": 3.0, + "selfdrive.ui.feedback.feedbackd": 1.0, "selfdrive.monitoring.dmonitoringd": 4.0, "./proclogd": 2.0, "system.logmessaged": 1.0, @@ -147,7 +148,7 @@ class TestOnroad: while not sm.seen['carState']: sm.update(1000) - route = params.get("CurrentRoute", encoding="utf-8") + route = params.get("CurrentRoute") assert route is not None segs = list(Path(Paths.log_root()).glob(f"{route}--*")) diff --git a/selfdrive/test/test_updated.py b/selfdrive/test/test_updated.py index ea945d94c2..95365640f5 100644 --- a/selfdrive/test/test_updated.py +++ b/selfdrive/test/test_updated.py @@ -105,7 +105,7 @@ class TestUpdated: ret = None start_time = time.monotonic() while ret is None: - ret = self.params.get(key, encoding='utf8') + ret = self.params.get(key) if time.monotonic() - start_time > timeout: break time.sleep(0.01) @@ -162,8 +162,7 @@ class TestUpdated: def _check_update_state(self, update_available): # make sure LastUpdateTime is recent - t = self._read_param("LastUpdateTime") - last_update_time = datetime.datetime.fromisoformat(t) + last_update_time = self._read_param("LastUpdateTime") td = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - last_update_time assert td.total_seconds() < 10 self.params.remove("LastUpdateTime") @@ -174,7 +173,7 @@ class TestUpdated: # check params update = self._read_param("UpdateAvailable") assert update == "1" == update_available, f"UpdateAvailable: {repr(update)}" - assert self._read_param("UpdateFailedCount") == "0" + assert self._read_param("UpdateFailedCount") == 0 # TODO: check that the finalized update actually matches remote # check the .overlay_init and .overlay_consistent flags diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index d883ca5b15..2a88832ef2 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -70,18 +70,8 @@ if GetOption('extras'): qt_src.remove("main.cc") # replaced by test_runner qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs) - qt_env.SharedLibrary("qt/python_helpers", ["qt/qt_window.cc"], LIBS=qt_libs) - - # setup and factory resetter - qt_env.Program("qt/setup/reset", ["qt/setup/reset.cc"], LIBS=qt_libs) - qt_env.Program("qt/setup/setup", ["qt/setup/setup.cc", asset_obj], - LIBS=qt_libs + ['curl', 'common']) - - # build updater UI - qt_env.Program("qt/setup/updater", ["qt/setup/updater.cc", asset_obj], LIBS=qt_libs) - + # build installers if arch != "Darwin": - # build installers raylib_env = env.Clone() raylib_env['LIBPATH'] += [f'#third_party/raylib/{arch}/'] raylib_env['LINKFLAGS'].append('-Wl,-strip-debug') @@ -113,7 +103,3 @@ if GetOption('extras'): f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter], LIBS=raylib_libs) # keep installers small assert f[0].get_size() < 1900*1e3, f[0].get_size() - -# build watch3 -if arch in ['x86_64', 'aarch64', 'Darwin'] or GetOption('extras'): - qt_env.Program("watch3", ["watch3.cc"], LIBS=qt_libs + ['common', 'msgq', 'visionipc']) diff --git a/selfdrive/ui/feedback/feedbackd.py b/selfdrive/ui/feedback/feedbackd.py new file mode 100755 index 0000000000..e814106304 --- /dev/null +++ b/selfdrive/ui/feedback/feedbackd.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +import cereal.messaging as messaging +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog +from cereal import car +from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER + +FEEDBACK_MAX_DURATION = 10.0 +ButtonType = car.CarState.ButtonEvent.Type + + +def main(): + params = Params() + pm = messaging.PubMaster(['userBookmark', 'audioFeedback']) + sm = messaging.SubMaster(['rawAudioData', 'bookmarkButton', 'carState', 'selfdriveStateSP']) + should_record_audio = False + block_num = 0 + waiting_for_release = False + early_stop_triggered = False + + while True: + sm.update() + should_send_bookmark = False + + # only allow the LKAS button to record feedback when MADS is disabled + if sm.updated['carState'] and sm['carState'].canValid and not sm['selfdriveStateSP'].mads.available: + for be in sm['carState'].buttonEvents: + if be.type == ButtonType.lkas: + if be.pressed: + if not should_record_audio: + if params.get_bool("RecordAudioFeedback"): # Start recording on first press if toggle set + should_record_audio = True + block_num = 0 + waiting_for_release = False + early_stop_triggered = False + cloudlog.info("LKAS button pressed - starting 10-second audio feedback") + else: + should_send_bookmark = True # immediately send bookmark if toggle false + cloudlog.info("LKAS button pressed - bookmarking") + elif should_record_audio and not waiting_for_release: # Wait for release of second press to stop recording early + waiting_for_release = True + elif waiting_for_release: # Second press released + waiting_for_release = False + early_stop_triggered = True + cloudlog.info("LKAS button released - ending recording early") + + if should_record_audio and sm.updated['rawAudioData']: + raw_audio = sm['rawAudioData'] + msg = messaging.new_message('audioFeedback', valid=True) + msg.audioFeedback.audio.data = raw_audio.data + msg.audioFeedback.audio.sampleRate = raw_audio.sampleRate + msg.audioFeedback.blockNum = block_num + block_num += 1 + if (block_num * SAMPLE_BUFFER / SAMPLE_RATE) >= FEEDBACK_MAX_DURATION or early_stop_triggered: # Check for timeout or early stop + should_send_bookmark = True # send bookmark at end of audio segment + should_record_audio = False + early_stop_triggered = False + cloudlog.info("10-second recording completed or second button press - stopping audio feedback") + pm.send('audioFeedback', msg) + + if sm.updated['bookmarkButton']: + cloudlog.info("Bookmark button pressed!") + should_send_bookmark = True + + if should_send_bookmark: + msg = messaging.new_message('userBookmark', valid=True) + pm.send('userBookmark', msg) + + +if __name__ == '__main__': + main() diff --git a/selfdrive/ui/layouts/home.py b/selfdrive/ui/layouts/home.py index 0fa91da552..f102481c3a 100644 --- a/selfdrive/ui/layouts/home.py +++ b/selfdrive/ui/layouts/home.py @@ -9,7 +9,7 @@ from openpilot.selfdrive.ui.widgets.prime import PrimeWidget from openpilot.selfdrive.ui.widgets.setup import SetupWidget from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget HEADER_HEIGHT = 80 HEAD_BUTTON_FONT_SIZE = 40 @@ -67,7 +67,7 @@ class HomeLayout(Widget): self.current_state = state def _render(self, rect: rl.Rectangle): - current_time = time.time() + current_time = time.monotonic() if current_time - self.last_refresh >= REFRESH_INTERVAL: self._refresh() self.last_refresh = current_time @@ -210,5 +210,5 @@ class HomeLayout(Widget): def _get_version_text(self) -> str: brand = "openpilot" - description = self.params.get("UpdaterCurrentDescription", encoding='utf-8') + description = self.params.get("UpdaterCurrentDescription") return f"{brand} {description}" if description else brand diff --git a/selfdrive/ui/layouts/main.py b/selfdrive/ui/layouts/main.py index a3081143f4..3ab8525d33 100644 --- a/selfdrive/ui/layouts/main.py +++ b/selfdrive/ui/layouts/main.py @@ -4,9 +4,9 @@ import cereal.messaging as messaging from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH from openpilot.selfdrive.ui.layouts.home import HomeLayout from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType -from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView -from openpilot.system.ui.lib.widget import Widget +from openpilot.selfdrive.ui.ui_state import device, ui_state +from openpilot.system.ui.widgets import Widget class MainState(IntEnum): @@ -19,7 +19,7 @@ class MainLayout(Widget): def __init__(self): super().__init__() - self._pm = messaging.PubMaster(['userFlag']) + self._pm = messaging.PubMaster(['bookmarkButton']) self._sidebar = Sidebar() self._current_mode = MainState.HOME @@ -40,7 +40,7 @@ class MainLayout(Widget): def _setup_callbacks(self): self._sidebar.set_callbacks(on_settings=self._on_settings_clicked, - on_flag=self._on_flag_clicked) + on_flag=self._on_bookmark_clicked) self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE)) self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state) self._layouts[MainState.ONROAD].set_callbacks(on_click=self._on_onroad_clicked) @@ -76,10 +76,10 @@ class MainLayout(Widget): def _on_settings_clicked(self): self.open_settings(PanelType.DEVICE) - def _on_flag_clicked(self): - user_flag = messaging.new_message('userFlag') - user_flag.valid = True - self._pm.send('userFlag', user_flag) + def _on_bookmark_clicked(self): + user_bookmark = messaging.new_message('bookmarkButton') + user_bookmark.valid = True + self._pm.send('bookmarkButton', user_bookmark) def _on_onroad_clicked(self): self._sidebar.set_visible(not self._sidebar.is_visible) diff --git a/selfdrive/ui/layouts/network.py b/selfdrive/ui/layouts/network.py index c227facd04..856be26e97 100644 --- a/selfdrive/ui/layouts/network.py +++ b/selfdrive/ui/layouts/network.py @@ -1,6 +1,6 @@ import pyray as rl -from openpilot.system.ui.lib.widget import Widget from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper +from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.network import WifiManagerUI diff --git a/selfdrive/ui/layouts/settings/developer.py b/selfdrive/ui/layouts/settings/developer.py index ce6e9e4af6..cfef0f84d1 100644 --- a/selfdrive/ui/layouts/settings/developer.py +++ b/selfdrive/ui/layouts/settings/developer.py @@ -1,8 +1,8 @@ -from openpilot.system.ui.lib.list_view import toggle_item -from openpilot.system.ui.lib.scroller import Scroller -from openpilot.system.ui.lib.widget import Widget from openpilot.common.params import Params from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.list_view import toggle_item +from openpilot.system.ui.widgets.scroller import Scroller # Description constants DESCRIPTIONS = { diff --git a/selfdrive/ui/layouts/settings/device.py b/selfdrive/ui/layouts/settings/device.py index 69b7822ba9..df8bba030c 100644 --- a/selfdrive/ui/layouts/settings/device.py +++ b/selfdrive/ui/layouts/settings/device.py @@ -5,15 +5,15 @@ from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog from openpilot.system.hardware import TICI from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.list_view import text_item, button_item, dual_button_item -from openpilot.system.ui.lib.scroller import Scroller -from openpilot.system.ui.lib.widget import Widget, DialogResult -from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog -from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog +from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog, alert_dialog from openpilot.system.ui.widgets.html_render import HtmlRenderer +from openpilot.system.ui.widgets.list_view import text_item, button_item, dual_button_item +from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog +from openpilot.system.ui.widgets.scroller import Scroller # Description constants DESCRIPTIONS = { @@ -41,7 +41,7 @@ class DeviceLayout(Widget): self._scroller = Scroller(items, line_separator=True, spacing=0) def _initialize_items(self): - dongle_id = self._params.get("DongleId", encoding="utf-8") or "N/A" + dongle_id = self._params.get("DongleId") or "N/A" serial = self._params.get("HardwareSerial") or "N/A" items = [ diff --git a/selfdrive/ui/layouts/settings/firehose.py b/selfdrive/ui/layouts/settings/firehose.py index 298f66e276..74a8f317d5 100644 --- a/selfdrive/ui/layouts/settings/firehose.py +++ b/selfdrive/ui/layouts/settings/firehose.py @@ -1,17 +1,16 @@ import pyray as rl -import json import time import threading from openpilot.common.api import Api, api_get from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.widget import Widget -from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.widgets import Widget TITLE = "Firehose Mode" @@ -51,11 +50,11 @@ class FirehoseLayout(Widget): self.last_update_time = 0 def _get_segment_count(self) -> int: - stats = self.params.get(self.PARAM_KEY, encoding='utf8') + stats = self.params.get(self.PARAM_KEY) if not stats: return 0 try: - return int(json.loads(stats).get("firehose", 0)) + return int(stats.get("firehose", 0)) except Exception: cloudlog.exception(f"Failed to decode firehose stats: {stats}") return 0 @@ -161,7 +160,7 @@ class FirehoseLayout(Widget): def _fetch_firehose_stats(self): try: - dongle_id = self.params.get("DongleId", encoding='utf8') + dongle_id = self.params.get("DongleId") if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: return identity_token = Api(dongle_id).get_token() @@ -169,7 +168,7 @@ class FirehoseLayout(Widget): if response.status_code == 200: data = response.json() self.segment_count = data.get("firehose", 0) - self.params.put(self.PARAM_KEY, json.dumps(data)) + self.params.put(self.PARAM_KEY, data) except Exception as e: cloudlog.error(f"Failed to fetch firehose stats: {e}") diff --git a/selfdrive/ui/layouts/settings/settings.py b/selfdrive/ui/layouts/settings/settings.py index d4dc420b7f..a322f21e80 100644 --- a/selfdrive/ui/layouts/settings/settings.py +++ b/selfdrive/ui/layouts/settings/settings.py @@ -2,6 +2,7 @@ import pyray as rl from dataclasses import dataclass from enum import IntEnum from collections.abc import Callable +from openpilot.selfdrive.ui.layouts.network import NetworkLayout from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout @@ -9,12 +10,12 @@ from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.selfdrive.ui.layouts.network import NetworkLayout -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget -# Import individual panels +# Settings close button +SETTINGS_CLOSE_TEXT = "×" +SETTINGS_CLOSE_TEXT_Y_OFFSET = 8 # The '×' character isn't quite vertically centered in the font so we need to offset it a bit to fully center it -SETTINGS_CLOSE_TEXT = "X" # Constants SIDEBAR_WIDTH = 500 CLOSE_BTN_SIZE = 200 @@ -62,7 +63,6 @@ class SettingsLayout(Widget): } self._font_medium = gui_app.font(FontWeight.MEDIUM) - self._font_bold = gui_app.font(FontWeight.SEMI_BOLD) # Callbacks self._close_callback: Callable | None = None @@ -84,7 +84,7 @@ class SettingsLayout(Widget): # Close button close_btn_rect = rl.Rectangle( - rect.x + (rect.width - CLOSE_BTN_SIZE) / 2, rect.y + 45, CLOSE_BTN_SIZE, CLOSE_BTN_SIZE + rect.x + (rect.width - CLOSE_BTN_SIZE) / 2, rect.y + 60, CLOSE_BTN_SIZE, CLOSE_BTN_SIZE ) pressed = (rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and @@ -92,12 +92,12 @@ class SettingsLayout(Widget): close_color = CLOSE_BTN_PRESSED if pressed else CLOSE_BTN_COLOR rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color) - close_text_size = measure_text_cached(self._font_bold, SETTINGS_CLOSE_TEXT, 140) + close_text_size = measure_text_cached(self._font_medium, SETTINGS_CLOSE_TEXT, 140) close_text_pos = rl.Vector2( close_btn_rect.x + (close_btn_rect.width - close_text_size.x) / 2, - close_btn_rect.y + (close_btn_rect.height - close_text_size.y) / 2, + close_btn_rect.y + (close_btn_rect.height - close_text_size.y) / 2 - SETTINGS_CLOSE_TEXT_Y_OFFSET, ) - rl.draw_text_ex(self._font_bold, SETTINGS_CLOSE_TEXT, close_text_pos, 140, 0, TEXT_SELECTED) + rl.draw_text_ex(self._font_medium, SETTINGS_CLOSE_TEXT, close_text_pos, 140, 0, TEXT_SELECTED) # Store close button rect for click detection self._close_btn_rect = close_btn_rect diff --git a/selfdrive/ui/layouts/settings/software.py b/selfdrive/ui/layouts/settings/software.py index b97e306ff9..4361725a1b 100644 --- a/selfdrive/ui/layouts/settings/software.py +++ b/selfdrive/ui/layouts/settings/software.py @@ -1,9 +1,9 @@ from openpilot.common.params import Params from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.list_view import button_item, text_item -from openpilot.system.ui.lib.scroller import Scroller -from openpilot.system.ui.lib.widget import Widget, DialogResult +from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog +from openpilot.system.ui.widgets.list_view import button_item, text_item +from openpilot.system.ui.widgets.scroller import Scroller class SoftwareLayout(Widget): diff --git a/selfdrive/ui/layouts/settings/toggles.py b/selfdrive/ui/layouts/settings/toggles.py index 7374d0f2ae..ff0564a61a 100644 --- a/selfdrive/ui/layouts/settings/toggles.py +++ b/selfdrive/ui/layouts/settings/toggles.py @@ -1,7 +1,7 @@ -from openpilot.system.ui.lib.list_view import multiple_button_item, toggle_item -from openpilot.system.ui.lib.scroller import Scroller -from openpilot.system.ui.lib.widget import Widget from openpilot.common.params import Params +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item +from openpilot.system.ui.widgets.scroller import Scroller # Description constants DESCRIPTIONS = { @@ -23,6 +23,10 @@ DESCRIPTIONS = { 'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.", "IsMetric": "Display speed in km/h instead of mph.", "RecordAudio": "Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.", + "RecordAudioFeedback": ( + "Press the LKAS button to record audio feedback about openpilot. When this toggle is disabled, the button acts as a bookmark button. " + + "The event will be highlighted in comma connect and the segment will be preserved on your device's storage." + ), } @@ -54,7 +58,7 @@ class TogglesLayout(Widget): buttons=["Aggressive", "Standard", "Relaxed"], button_width=255, callback=self._set_longitudinal_personality, - selected_index=int(self._params.get("LongitudinalPersonality") or 0), + selected_index=self._params.get("LongitudinalPersonality", return_default=True), icon="speed_limit.png" ), toggle_item( @@ -82,7 +86,13 @@ class TogglesLayout(Widget): icon="microphone.png", ), toggle_item( - "Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="monitoring.png" + "Record Audio Feedback with LKAS button", + DESCRIPTIONS["RecordAudioFeedback"], + self._params.get_bool("RecordAudioFeedback"), + icon="microphone.png", + ), + toggle_item( + "Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="metric.png" ), ] @@ -92,4 +102,4 @@ class TogglesLayout(Widget): self._scroller.render(rect) def _set_longitudinal_personality(self, button_index: int): - self._params.put("LongitudinalPersonality", str(button_index)) + self._params.put("LongitudinalPersonality", button_index) diff --git a/selfdrive/ui/layouts/sidebar.py b/selfdrive/ui/layouts/sidebar.py index cb8c6d472e..6af947a104 100644 --- a/selfdrive/ui/layouts/sidebar.py +++ b/selfdrive/ui/layouts/sidebar.py @@ -6,12 +6,13 @@ from cereal import log from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget SIDEBAR_WIDTH = 300 METRIC_HEIGHT = 126 METRIC_WIDTH = 240 METRIC_MARGIN = 30 +FONT_SIZE = 35 SETTINGS_BTN = rl.Rectangle(50, 35, 200, 117) HOME_BTN = rl.Rectangle(60, 860, 180, 180) @@ -145,7 +146,7 @@ class Sidebar(Widget): def _draw_buttons(self, rect: rl.Rectangle): mouse_pos = rl.get_mouse_position() - mouse_down = self._is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) + mouse_down = self.is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) # Settings button settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN) @@ -175,7 +176,7 @@ class Sidebar(Widget): # Network type text text_y = rect.y + 247 text_pos = rl.Vector2(rect.x + 58, text_y) - rl.draw_text_ex(self._font_regular, self._net_type, text_pos, 35, 0, Colors.WHITE) + rl.draw_text_ex(self._font_regular, self._net_type, text_pos, FONT_SIZE, 0, Colors.WHITE) def _draw_metrics(self, rect: rl.Rectangle): metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)] @@ -194,11 +195,14 @@ class Sidebar(Widget): # Draw border rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.15, 10, 2, Colors.METRIC_BORDER) - # Draw text - text = f"{metric.label}\n{metric.value}" - text_size = measure_text_cached(self._font_bold, text, 35) - text_pos = rl.Vector2( - metric_rect.x + 22 + (metric_rect.width - 22 - text_size.x) / 2, - metric_rect.y + (metric_rect.height - text_size.y) / 2 - ) - rl.draw_text_ex(self._font_bold, text, text_pos, 35, 0, Colors.WHITE) + # Draw label and value + labels = [metric.label, metric.value] + text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE) + for text in labels: + text_size = measure_text_cached(self._font_bold, text, FONT_SIZE) + text_y += text_size.y + text_pos = rl.Vector2( + metric_rect.x + 22 + (metric_rect.width - 22 - text_size.x) / 2, + text_y + ) + rl.draw_text_ex(self._font_bold, text, text_pos, FONT_SIZE, 0, Colors.WHITE) diff --git a/selfdrive/ui/lib/prime_state.py b/selfdrive/ui/lib/prime_state.py index 6b87540f65..b6c0d88469 100644 --- a/selfdrive/ui/lib/prime_state.py +++ b/selfdrive/ui/lib/prime_state.py @@ -35,7 +35,7 @@ class PrimeState: self.start() def _load_initial_state(self) -> PrimeType: - prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType", encoding='utf8') + prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType") try: if prime_type_str is not None: return PrimeType(int(prime_type_str)) @@ -44,7 +44,7 @@ class PrimeState: return PrimeType.UNKNOWN def _fetch_prime_status(self) -> None: - dongle_id = self._params.get("DongleId", encoding='utf8') + dongle_id = self._params.get("DongleId") if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: return @@ -63,7 +63,7 @@ class PrimeState: with self._lock: if prime_type != self.prime_type: self.prime_type = prime_type - self._params.put("PrimeType", str(int(prime_type))) + self._params.put("PrimeType", int(prime_type)) cloudlog.info(f"Prime type updated to {prime_type}") def _worker_thread(self) -> None: diff --git a/selfdrive/ui/onroad/alert_renderer.py b/selfdrive/ui/onroad/alert_renderer.py index 7a17291bbe..e8817f24b4 100644 --- a/selfdrive/ui/onroad/alert_renderer.py +++ b/selfdrive/ui/onroad/alert_renderer.py @@ -2,12 +2,15 @@ import time import pyray as rl from dataclasses import dataclass from cereal import messaging, log +from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.hardware import TICI from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_FPS -from openpilot.system.ui.lib.label import gui_text_box from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.widget import Widget -from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.label import gui_text_box + +AlertSize = log.SelfdriveState.AlertSize +AlertStatus = log.SelfdriveState.AlertStatus ALERT_MARGIN = 40 ALERT_PADDING = 60 @@ -23,9 +26,9 @@ SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds # Constants ALERT_COLORS = { - log.SelfdriveState.AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black - log.SelfdriveState.AlertStatus.userPrompt: rl.Color(0xFE, 0x8C, 0x34, 235), # Orange - log.SelfdriveState.AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 235), # Red + AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black + AlertStatus.userPrompt: rl.Color(0xFE, 0x8C, 0x34, 235), # Orange + AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 235), # Red } @@ -41,22 +44,22 @@ class Alert: ALERT_STARTUP_PENDING = Alert( text1="openpilot Unavailable", text2="Waiting to start", - size=log.SelfdriveState.AlertSize.mid, - status=log.SelfdriveState.AlertStatus.normal, + size=AlertSize.mid, + status=AlertStatus.normal, ) ALERT_CRITICAL_TIMEOUT = Alert( text1="TAKE CONTROL IMMEDIATELY", text2="System Unresponsive", - size=log.SelfdriveState.AlertSize.full, - status=log.SelfdriveState.AlertStatus.critical, + size=AlertSize.full, + status=AlertStatus.critical, ) ALERT_CRITICAL_REBOOT = Alert( text1="System Unresponsive", text2="Reboot Device", - size=log.SelfdriveState.AlertSize.full, - status=log.SelfdriveState.AlertStatus.critical, + size=AlertSize.full, + status=AlertStatus.critical, ) @@ -93,7 +96,7 @@ class AlertRenderer(Widget): return None # Return current alert - return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize, status=ss.alertStatus) + return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize.raw, status=ss.alertStatus.raw) def _render(self, rect: rl.Rectangle) -> bool: alert = self.get_alert(ui_state.sm) @@ -113,10 +116,10 @@ class AlertRenderer(Widget): return True def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle: - if size == log.SelfdriveState.AlertSize.full: + if size == AlertSize.full: return rect - height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == log.SelfdriveState.AlertSize.small else + height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == AlertSize.small else ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING) return rl.Rectangle( @@ -127,19 +130,19 @@ class AlertRenderer(Widget): ) def _draw_background(self, rect: rl.Rectangle, alert: Alert) -> None: - color = ALERT_COLORS.get(alert.status, ALERT_COLORS[log.SelfdriveState.AlertStatus.normal]) + color = ALERT_COLORS.get(alert.status, ALERT_COLORS[AlertStatus.normal]) - if alert.size != log.SelfdriveState.AlertSize.full: + if alert.size != AlertSize.full: roundness = ALERT_BORDER_RADIUS / (min(rect.width, rect.height) / 2) rl.draw_rectangle_rounded(rect, roundness, 10, color) else: rl.draw_rectangle_rec(rect, color) def _draw_text(self, rect: rl.Rectangle, alert: Alert) -> None: - if alert.size == log.SelfdriveState.AlertSize.small: + if alert.size == AlertSize.small: self._draw_centered(alert.text1, rect, self.font_bold, ALERT_FONT_MEDIUM) - elif alert.size == log.SelfdriveState.AlertSize.mid: + elif alert.size == AlertSize.mid: self._draw_centered(alert.text1, rect, self.font_bold, ALERT_FONT_BIG, center_y=False) rect.y += ALERT_FONT_BIG + ALERT_LINE_SPACING self._draw_centered(alert.text2, rect, self.font_regular, ALERT_FONT_SMALL, center_y=False) diff --git a/selfdrive/ui/onroad/cameraview.py b/selfdrive/ui/onroad/cameraview.py index 33f1961d2b..ca031e2f17 100644 --- a/selfdrive/ui/onroad/cameraview.py +++ b/selfdrive/ui/onroad/cameraview.py @@ -2,12 +2,12 @@ import platform import numpy as np import pyray as rl -from openpilot.system.hardware import TICI from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from openpilot.common.swaglog import cloudlog +from openpilot.system.hardware import TICI from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts @@ -141,6 +141,9 @@ class CameraView(Widget): self.client = None + def __del__(self): + self.close() + def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: if not self.frame: return np.eye(3) @@ -179,6 +182,9 @@ class CameraView(Widget): transform = self._calc_frame_matrix(rect) src_rect = rl.Rectangle(0, 0, float(self.frame.width), float(self.frame.height)) + # Flip driver camera horizontally + if self._stream_type == VisionStreamType.VISION_STREAM_DRIVER: + src_rect.width = -src_rect.width # Calculate scale scale_x = rect.width * transform[0, 0] # zx @@ -334,16 +340,7 @@ class CameraView(Widget): if __name__ == "__main__": - gui_app.init_window("watch3") - road_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) - driver_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER) - wide_road_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD) - try: - for _ in gui_app.render(): - road_camera_view.render(rl.Rectangle(gui_app.width // 4, 0, gui_app.width // 2, gui_app.height // 2)) - driver_camera_view.render(rl.Rectangle(0, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2)) - wide_road_camera_view.render(rl.Rectangle(gui_app.width // 2, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2)) - finally: - road_camera_view.close() - driver_camera_view.close() - wide_road_camera_view.close() + gui_app.init_window("camera view") + road = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) + for _ in gui_app.render(): + road.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) diff --git a/selfdrive/ui/onroad/driver_camera_dialog.py b/selfdrive/ui/onroad/driver_camera_dialog.py index 40c72f292b..c8b4e62030 100644 --- a/selfdrive/ui/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/onroad/driver_camera_dialog.py @@ -5,7 +5,7 @@ from openpilot.selfdrive.ui.onroad.cameraview import CameraView from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.label import gui_label +from openpilot.system.ui.widgets.label import gui_label class DriverCameraDialog(CameraView): diff --git a/selfdrive/ui/onroad/driver_state.py b/selfdrive/ui/onroad/driver_state.py index 8d5fb951d0..a25d9bd316 100644 --- a/selfdrive/ui/onroad/driver_state.py +++ b/selfdrive/ui/onroad/driver_state.py @@ -3,7 +3,7 @@ import pyray as rl from dataclasses import dataclass from openpilot.selfdrive.ui.ui_state import ui_state, UI_BORDER_SIZE from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget # Default 3D coordinates for face keypoints as a NumPy array DEFAULT_FACE_KPTS_3D = np.array([ diff --git a/selfdrive/ui/onroad/exp_button.py b/selfdrive/ui/onroad/exp_button.py index 8c835090d0..27f5763077 100644 --- a/selfdrive/ui/onroad/exp_button.py +++ b/selfdrive/ui/onroad/exp_button.py @@ -1,9 +1,9 @@ import time import pyray as rl +from openpilot.common.params import Params from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.widget import Widget -from openpilot.common.params import Params +from openpilot.system.ui.widgets import Widget class ExpButton(Widget): @@ -41,7 +41,7 @@ class ExpButton(Widget): # Hold new state temporarily self._held_mode = new_mode - self._hold_end_time = time.time() + self._hold_duration + self._hold_end_time = time.monotonic() + self._hold_duration return True return False @@ -50,7 +50,7 @@ class ExpButton(Widget): center_y = int(self._rect.y + self._rect.height // 2) mouse_over = rl.check_collision_point_rec(rl.get_mouse_position(), self._rect) - mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed + mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed self._white_color.a = 180 if (mouse_down and mouse_over) or not self._engageable else 255 texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel @@ -58,7 +58,7 @@ class ExpButton(Widget): rl.draw_texture(texture, center_x - texture.width // 2, center_y - texture.height // 2, self._white_color) def _held_or_actual_mode(self): - now = time.time() + now = time.monotonic() if self._hold_end_time and now < self._hold_end_time: return self._held_mode diff --git a/selfdrive/ui/onroad/hud_renderer.py b/selfdrive/ui/onroad/hud_renderer.py index da292d38d5..4ae713d9db 100644 --- a/selfdrive/ui/onroad/hud_renderer.py +++ b/selfdrive/ui/onroad/hud_renderer.py @@ -1,11 +1,11 @@ import pyray as rl from dataclasses import dataclass -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus +from openpilot.common.constants import CV from openpilot.selfdrive.ui.onroad.exp_button import ExpButton +from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.common.conversions import Conversions as CV -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget # Constants SET_SPEED_NA = 255 diff --git a/selfdrive/ui/onroad/model_renderer.py b/selfdrive/ui/onroad/model_renderer.py index ab2b7aaefb..4439141a40 100644 --- a/selfdrive/ui/onroad/model_renderer.py +++ b/selfdrive/ui/onroad/model_renderer.py @@ -4,11 +4,11 @@ import pyray as rl from cereal import messaging, car from dataclasses import dataclass, field from openpilot.common.params import Params +from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import DEFAULT_FPS from openpilot.system.ui.lib.shader_polygon import draw_polygon -from openpilot.system.ui.lib.widget import Widget -from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT +from openpilot.system.ui.widgets import Widget CLIP_MARGIN = 500 MIN_DRAW_DISTANCE = 10.0 diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 205a14624a..f8a46257f6 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -75,6 +75,13 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { "../assets/icons/microphone.png", true, }, + { + "RecordAudioFeedback", + tr("Record Audio Feedback with LKAS button"), + tr("Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage.\n\nNote that this feature is only compatible with select cars."), + "../assets/icons/microphone.png", + false, + }, { "IsMetric", tr("Use Metric System"), diff --git a/selfdrive/ui/qt/python_helpers.py b/selfdrive/ui/qt/python_helpers.py deleted file mode 100644 index 1f6d43f309..0000000000 --- a/selfdrive/ui/qt/python_helpers.py +++ /dev/null @@ -1,23 +0,0 @@ -import os -import platform -from cffi import FFI - -import sip - -from openpilot.common.basedir import BASEDIR - -def suffix(): - return ".dylib" if platform.system() == "Darwin" else ".so" - - -def get_ffi(): - lib = os.path.join(BASEDIR, "selfdrive", "ui", "qt", "libpython_helpers" + suffix()) - - ffi = FFI() - ffi.cdef("void set_main_window(void *w);") - return ffi, ffi.dlopen(lib) - - -def set_main_window(widget): - ffi, lib = get_ffi() - lib.set_main_window(ffi.cast('void*', sip.unwrapinstance(widget))) diff --git a/selfdrive/ui/qt/setup/reset.cc b/selfdrive/ui/qt/setup/reset.cc deleted file mode 100644 index 82fd5a7820..0000000000 --- a/selfdrive/ui/qt/setup/reset.cc +++ /dev/null @@ -1,141 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/setup/reset.h" - -#define NVME "/dev/nvme0n1" -#define USERDATA "/dev/disk/by-partlabel/userdata" - -void Reset::doErase() { - // best effort to wipe nvme - std::system("sudo umount " NVME); - std::system("yes | sudo mkfs.ext4 " NVME); - - int rm = std::system("sudo rm -rf /data/*"); - std::system("sudo umount " USERDATA); - int fmt = std::system("yes | sudo mkfs.ext4 " USERDATA); - - if (rm == 0 || fmt == 0) { - std::system("sudo reboot"); - } - body->setText(tr("Reset failed. Reboot to try again.")); - rebootBtn->show(); -} - -void Reset::startReset() { - body->setText(tr("Resetting device...\nThis may take up to a minute.")); - rejectBtn->hide(); - rebootBtn->hide(); - confirmBtn->hide(); -#ifdef __aarch64__ - QTimer::singleShot(100, this, &Reset::doErase); -#endif -} - -void Reset::confirm() { - const QString confirm_txt = tr("Are you sure you want to reset your device?"); - if (body->text() != confirm_txt) { - body->setText(confirm_txt); - } else { - startReset(); - } -} - -Reset::Reset(ResetMode mode, QWidget *parent) : QWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(45, 220, 45, 45); - main_layout->setSpacing(0); - - QLabel *title = new QLabel(tr("System Reset")); - title->setStyleSheet("font-size: 90px; font-weight: 600;"); - main_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - - main_layout->addSpacing(60); - - body = new QLabel(tr("System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot.")); - body->setWordWrap(true); - body->setStyleSheet("font-size: 80px; font-weight: light;"); - main_layout->addWidget(body, 1, Qt::AlignTop | Qt::AlignLeft); - - QHBoxLayout *blayout = new QHBoxLayout(); - main_layout->addLayout(blayout); - blayout->setSpacing(50); - - rejectBtn = new QPushButton(tr("Cancel")); - blayout->addWidget(rejectBtn); - QObject::connect(rejectBtn, &QPushButton::clicked, QCoreApplication::instance(), &QCoreApplication::quit); - - rebootBtn = new QPushButton(tr("Reboot")); - blayout->addWidget(rebootBtn); -#ifdef __aarch64__ - QObject::connect(rebootBtn, &QPushButton::clicked, [=]{ - std::system("sudo reboot"); - }); -#endif - - confirmBtn = new QPushButton(tr("Confirm")); - confirmBtn->setStyleSheet(R"( - QPushButton { - background-color: #465BEA; - } - QPushButton:pressed { - background-color: #3049F4; - } - )"); - blayout->addWidget(confirmBtn); - QObject::connect(confirmBtn, &QPushButton::clicked, this, &Reset::confirm); - - bool recover = mode == ResetMode::RECOVER; - rejectBtn->setVisible(!recover); - rebootBtn->setVisible(recover); - if (recover) { - body->setText(tr("Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device.")); - } - - // automatically start if we're just finishing up an ABL reset - if (mode == ResetMode::FORMAT) { - startReset(); - } - - setStyleSheet(R"( - * { - font-family: Inter; - color: white; - background-color: black; - } - QLabel { - margin-left: 140; - } - QPushButton { - height: 160; - font-size: 55px; - font-weight: 400; - border-radius: 10px; - background-color: #333333; - } - QPushButton:pressed { - background-color: #444444; - } - )"); -} - -int main(int argc, char *argv[]) { - ResetMode mode = ResetMode::USER_RESET; - if (argc > 1) { - if (strcmp(argv[1], "--recover") == 0) { - mode = ResetMode::RECOVER; - } else if (strcmp(argv[1], "--format") == 0) { - mode = ResetMode::FORMAT; - } - } - - QApplication a(argc, argv); - Reset reset(mode); - setMainWindow(&reset); - return a.exec(); -} diff --git a/selfdrive/ui/qt/setup/reset.h b/selfdrive/ui/qt/setup/reset.h deleted file mode 100644 index 2e0784cdc9..0000000000 --- a/selfdrive/ui/qt/setup/reset.h +++ /dev/null @@ -1,27 +0,0 @@ -#include -#include -#include - -enum ResetMode { - USER_RESET, // user initiated a factory reset from openpilot - RECOVER, // userdata is corrupt for some reason, give a chance to recover - FORMAT, // finish up a factory reset from a tool that doesn't flash an empty partition to userdata -}; - -class Reset : public QWidget { - Q_OBJECT - -public: - explicit Reset(ResetMode mode, QWidget *parent = 0); - -private: - QLabel *body; - QPushButton *rejectBtn; - QPushButton *rebootBtn; - QPushButton *confirmBtn; - void doErase(); - void startReset(); - -private slots: - void confirm(); -}; diff --git a/selfdrive/ui/qt/setup/setup.cc b/selfdrive/ui/qt/setup/setup.cc deleted file mode 100644 index 90ed973b0d..0000000000 --- a/selfdrive/ui/qt/setup/setup.cc +++ /dev/null @@ -1,541 +0,0 @@ -#include "selfdrive/ui/qt/setup/setup.h" - -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include "common/util.h" -#include "system/hardware/hw.h" -#include "selfdrive/ui/qt/api.h" -#include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/network/networking.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/widgets/input.h" - -const std::string USER_AGENT = "AGNOSSetup-"; -const QString OPENPILOT_URL = "https://openpilot.comma.ai"; - -bool is_elf(char *fname) { - FILE *fp = fopen(fname, "rb"); - if (fp == NULL) { - return false; - } - char buf[4]; - size_t n = fread(buf, 1, 4, fp); - fclose(fp); - return n == 4 && buf[0] == 0x7f && buf[1] == 'E' && buf[2] == 'L' && buf[3] == 'F'; -} - -void Setup::download(QString url) { - // autocomplete incomplete urls - if (QRegularExpression("^([^/.]+)/([^/]+)$").match(url).hasMatch()) { - url.prepend("https://installer.comma.ai/"); - } - - CURL *curl = curl_easy_init(); - if (!curl) { - emit finished(url, tr("Something went wrong. Reboot the device.")); - return; - } - - auto version = util::read_file("/VERSION"); - - struct curl_slist *list = NULL; - std::string header = "X-openpilot-serial: " + Hardware::get_serial(); - list = curl_slist_append(list, header.c_str()); - - char tmpfile[] = "/tmp/installer_XXXXXX"; - FILE *fp = fdopen(mkstemp(tmpfile), "wb"); - - curl_easy_setopt(curl, CURLOPT_URL, url.toStdString().c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); - curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_USERAGENT, (USER_AGENT + version).c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); - - int ret = curl_easy_perform(curl); - long res_status = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &res_status); - - if (ret != CURLE_OK || res_status != 200) { - emit finished(url, tr("Ensure the entered URL is valid, and the device’s internet connection is good.")); - } else if (!is_elf(tmpfile)) { - emit finished(url, tr("No custom software found at this URL.")); - } else { - rename(tmpfile, "/tmp/installer"); - - FILE *fp_url = fopen("/tmp/installer_url", "w"); - fprintf(fp_url, "%s", url.toStdString().c_str()); - fclose(fp_url); - - emit finished(url); - } - - curl_slist_free_all(list); - curl_easy_cleanup(curl); - fclose(fp); -} - -QWidget * Setup::low_voltage() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 0, 55, 55); - main_layout->setSpacing(0); - - // inner text layout: warning icon, title, and body - QVBoxLayout *inner_layout = new QVBoxLayout(); - inner_layout->setContentsMargins(110, 144, 365, 0); - main_layout->addLayout(inner_layout); - - QLabel *triangle = new QLabel(); - triangle->setPixmap(QPixmap(ASSET_PATH + "icons/warning.png")); - inner_layout->addWidget(triangle, 0, Qt::AlignTop | Qt::AlignLeft); - inner_layout->addSpacing(80); - - QLabel *title = new QLabel(tr("WARNING: Low Voltage")); - title->setStyleSheet("font-size: 90px; font-weight: 500; color: #FF594F;"); - inner_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - - inner_layout->addSpacing(25); - - QLabel *body = new QLabel(tr("Power your device in a car with a harness or proceed at your own risk.")); - body->setWordWrap(true); - body->setAlignment(Qt::AlignTop | Qt::AlignLeft); - body->setStyleSheet("font-size: 80px; font-weight: 300;"); - inner_layout->addWidget(body); - - inner_layout->addStretch(); - - // power off + continue buttons - QHBoxLayout *blayout = new QHBoxLayout(); - blayout->setSpacing(50); - main_layout->addLayout(blayout, 0); - - QPushButton *poweroff = new QPushButton(tr("Power off")); - poweroff->setObjectName("navBtn"); - blayout->addWidget(poweroff); - QObject::connect(poweroff, &QPushButton::clicked, this, [=]() { - Hardware::poweroff(); - }); - - QPushButton *cont = new QPushButton(tr("Continue")); - cont->setObjectName("navBtn"); - blayout->addWidget(cont); - QObject::connect(cont, &QPushButton::clicked, this, &Setup::nextPage); - - return widget; -} - -QWidget * Setup::custom_software_warning() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 0, 55, 55); - main_layout->setSpacing(0); - - QVBoxLayout *inner_layout = new QVBoxLayout(); - inner_layout->setContentsMargins(110, 110, 300, 0); - main_layout->addLayout(inner_layout); - - QLabel *title = new QLabel(tr("WARNING: Custom Software")); - title->setStyleSheet("font-size: 90px; font-weight: 500; color: #FF594F;"); - inner_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - - inner_layout->addSpacing(25); - - QLabel *body = new QLabel(tr("Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.\n\nIf you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later.")); - body->setWordWrap(true); - body->setAlignment(Qt::AlignTop | Qt::AlignLeft); - body->setStyleSheet("font-size: 65px; font-weight: 300;"); - inner_layout->addWidget(body); - - inner_layout->addStretch(); - - QHBoxLayout *blayout = new QHBoxLayout(); - blayout->setSpacing(50); - main_layout->addLayout(blayout, 0); - - QPushButton *back = new QPushButton(tr("Back")); - back->setObjectName("navBtn"); - blayout->addWidget(back); - QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage); - - QPushButton *cont = new QPushButton(tr("Continue")); - cont->setObjectName("navBtn"); - blayout->addWidget(cont); - QObject::connect(cont, &QPushButton::clicked, this, [=]() { - QTimer::singleShot(0, [=]() { - setCurrentWidget(downloading_widget); - }); - QString url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software")); - if (!url.isEmpty()) { - QTimer::singleShot(1000, this, [=]() { - download(url); - }); - } else { - setCurrentWidget(software_selection_widget); - } - }); - - return widget; -} - -QWidget * Setup::getting_started() { - QWidget *widget = new QWidget(); - - QHBoxLayout *main_layout = new QHBoxLayout(widget); - main_layout->setMargin(0); - - QVBoxLayout *vlayout = new QVBoxLayout(); - vlayout->setContentsMargins(165, 280, 100, 0); - main_layout->addLayout(vlayout); - - QLabel *title = new QLabel(tr("Getting Started")); - title->setStyleSheet("font-size: 90px; font-weight: 500;"); - vlayout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - - vlayout->addSpacing(90); - QLabel *desc = new QLabel(tr("Before we get on the road, let’s finish installation and cover some details.")); - desc->setWordWrap(true); - desc->setStyleSheet("font-size: 80px; font-weight: 300;"); - vlayout->addWidget(desc, 0, Qt::AlignTop | Qt::AlignLeft); - - vlayout->addStretch(); - - QPushButton *btn = new QPushButton(); - btn->setIcon(QIcon(":/images/button_continue_triangle.svg")); - btn->setIconSize(QSize(54, 106)); - btn->setFixedSize(310, 1080); - btn->setProperty("primary", true); - btn->setStyleSheet("border: none;"); - main_layout->addWidget(btn, 0, Qt::AlignRight); - QObject::connect(btn, &QPushButton::clicked, this, &Setup::nextPage); - - return widget; -} - -QWidget * Setup::network_setup() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 50, 55, 50); - - // title - QLabel *title = new QLabel(tr("Connect to Wi-Fi")); - title->setStyleSheet("font-size: 90px; font-weight: 500;"); - main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); - - main_layout->addSpacing(25); - - // wifi widget - Networking *networking = new Networking(this, false); - networking->setStyleSheet("Networking {background-color: #292929; border-radius: 13px;}"); - main_layout->addWidget(networking, 1); - - main_layout->addSpacing(35); - - // back + continue buttons - QHBoxLayout *blayout = new QHBoxLayout; - main_layout->addLayout(blayout); - blayout->setSpacing(50); - - QPushButton *back = new QPushButton(tr("Back")); - back->setObjectName("navBtn"); - QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage); - blayout->addWidget(back); - - QPushButton *cont = new QPushButton(); - cont->setObjectName("navBtn"); - cont->setProperty("primary", true); - cont->setEnabled(false); - QObject::connect(cont, &QPushButton::clicked, this, &Setup::nextPage); - blayout->addWidget(cont); - - // setup timer for testing internet connection - HttpRequest *request = new HttpRequest(this, false, 2500); - QObject::connect(request, &HttpRequest::requestDone, [=](const QString &, bool success) { - cont->setEnabled(success); - if (success) { - const bool wifi = networking->wifi->currentNetworkType() == NetworkType::WIFI; - cont->setText(wifi ? tr("Continue") : tr("Continue without Wi-Fi")); - } else { - cont->setText(tr("Waiting for internet")); - } - repaint(); - }); - request->sendRequest(OPENPILOT_URL); - QTimer *timer = new QTimer(this); - QObject::connect(timer, &QTimer::timeout, [=]() { - if (!request->active() && cont->isVisible()) { - request->sendRequest(OPENPILOT_URL); - } - }); - timer->start(1000); - - return widget; -} - -QWidget * radio_button(QString title, QButtonGroup *group) { - QPushButton *btn = new QPushButton(title); - btn->setCheckable(true); - group->addButton(btn); - btn->setStyleSheet(R"( - QPushButton { - height: 230; - padding-left: 100px; - padding-right: 100px; - text-align: left; - font-size: 80px; - font-weight: 400; - border-radius: 10px; - background-color: #4F4F4F; - } - QPushButton:checked { - background-color: #465BEA; - } - )"); - - // checkmark icon - QPixmap pix(":/icons/circled_check.svg"); - btn->setIcon(pix); - btn->setIconSize(QSize(0, 0)); - btn->setLayoutDirection(Qt::RightToLeft); - QObject::connect(btn, &QPushButton::toggled, [=](bool checked) { - btn->setIconSize(checked ? QSize(104, 104) : QSize(0, 0)); - }); - return btn; -} - -QWidget * Setup::software_selection() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 50, 55, 50); - main_layout->setSpacing(0); - - // title - QLabel *title = new QLabel(tr("Choose Software to Install")); - title->setStyleSheet("font-size: 90px; font-weight: 500;"); - main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); - - main_layout->addSpacing(50); - - // sunnypilot + custom radio buttons - QButtonGroup *group = new QButtonGroup(widget); - group->setExclusive(true); - - QWidget *openpilot = radio_button(tr("sunnypilot"), group); - main_layout->addWidget(openpilot); - - main_layout->addSpacing(30); - - QWidget *custom = radio_button(tr("Custom Software"), group); - main_layout->addWidget(custom); - - main_layout->addStretch(); - - // back + continue buttons - QHBoxLayout *blayout = new QHBoxLayout; - main_layout->addLayout(blayout); - blayout->setSpacing(50); - - QPushButton *back = new QPushButton(tr("Back")); - back->setObjectName("navBtn"); - QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage); - blayout->addWidget(back); - - QPushButton *cont = new QPushButton(tr("Continue")); - cont->setObjectName("navBtn"); - cont->setEnabled(false); - cont->setProperty("primary", true); - blayout->addWidget(cont); - - QObject::connect(cont, &QPushButton::clicked, [=]() { - if (group->checkedButton() != openpilot) { - QTimer::singleShot(0, [=]() { - setCurrentWidget(custom_software_warning_widget); - }); - } else { - QTimer::singleShot(0, [=]() { - setCurrentWidget(downloading_widget); - }); - QTimer::singleShot(1000, this, [=]() { - download(OPENPILOT_URL); - }); - } - }); - - connect(group, QOverload::of(&QButtonGroup::buttonClicked), [=](QAbstractButton *btn) { - btn->setChecked(true); - cont->setEnabled(true); - }); - - return widget; -} - -QWidget * Setup::downloading() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - QLabel *txt = new QLabel(tr("Downloading...")); - txt->setStyleSheet("font-size: 90px; font-weight: 500;"); - main_layout->addWidget(txt, 0, Qt::AlignCenter); - return widget; -} - -QWidget * Setup::download_failed(QLabel *url, QLabel *body) { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 185, 55, 55); - main_layout->setSpacing(0); - - QLabel *title = new QLabel(tr("Download Failed")); - title->setStyleSheet("font-size: 90px; font-weight: 500;"); - main_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - - main_layout->addSpacing(67); - - url->setWordWrap(true); - url->setAlignment(Qt::AlignTop | Qt::AlignLeft); - url->setStyleSheet("font-family: \"JetBrains Mono\"; font-size: 64px; font-weight: 400; margin-right: 100px;"); - main_layout->addWidget(url); - - main_layout->addSpacing(48); - - body->setWordWrap(true); - body->setAlignment(Qt::AlignTop | Qt::AlignLeft); - body->setStyleSheet("font-size: 80px; font-weight: 300; margin-right: 100px;"); - main_layout->addWidget(body); - - main_layout->addStretch(); - - // reboot + start over buttons - QHBoxLayout *blayout = new QHBoxLayout(); - blayout->setSpacing(50); - main_layout->addLayout(blayout, 0); - - QPushButton *reboot = new QPushButton(tr("Reboot device")); - reboot->setObjectName("navBtn"); - blayout->addWidget(reboot); - QObject::connect(reboot, &QPushButton::clicked, this, [=]() { - Hardware::reboot(); - }); - - QPushButton *restart = new QPushButton(tr("Start over")); - restart->setObjectName("navBtn"); - restart->setProperty("primary", true); - blayout->addWidget(restart); - QObject::connect(restart, &QPushButton::clicked, this, [=]() { - setCurrentIndex(1); - }); - - widget->setStyleSheet(R"( - QLabel { - margin-left: 117; - } - )"); - return widget; -} - -void Setup::prevPage() { - setCurrentIndex(currentIndex() - 1); -} - -void Setup::nextPage() { - setCurrentIndex(currentIndex() + 1); -} - -Setup::Setup(QWidget *parent) : QStackedWidget(parent) { - if (std::getenv("MULTILANG")) { - selectLanguage(); - } - - std::stringstream buffer; - buffer << std::ifstream("/sys/class/hwmon/hwmon1/in1_input").rdbuf(); - float voltage = (float)std::atoi(buffer.str().c_str()) / 1000.; - if (voltage < 7) { - addWidget(low_voltage()); - } - - addWidget(getting_started()); - addWidget(network_setup()); - software_selection_widget = software_selection(); - addWidget(software_selection_widget); - custom_software_warning_widget = custom_software_warning(); - addWidget(custom_software_warning_widget); - downloading_widget = downloading(); - addWidget(downloading_widget); - - QLabel *url_label = new QLabel(); - QLabel *body_label = new QLabel(); - failed_widget = download_failed(url_label, body_label); - addWidget(failed_widget); - - QObject::connect(this, &Setup::finished, [=](const QString &url, const QString &error) { - qDebug() << "finished" << url << error; - if (error.isEmpty()) { - // hide setup on success - QTimer::singleShot(3000, this, &QWidget::hide); - } else { - url_label->setText(url); - body_label->setText(error); - setCurrentWidget(failed_widget); - } - }); - - // TODO: revisit pressed bg color - setStyleSheet(R"( - * { - color: white; - font-family: Inter; - } - Setup { - background-color: black; - } - QPushButton#navBtn { - height: 160; - font-size: 55px; - font-weight: 400; - border-radius: 10px; - background-color: #333333; - } - QPushButton#navBtn:disabled, QPushButton[primary='true']:disabled { - color: #808080; - background-color: #333333; - } - QPushButton#navBtn:pressed { - background-color: #444444; - } - QPushButton[primary='true'], #navBtn[primary='true'] { - background-color: #465BEA; - } - QPushButton[primary='true']:pressed, #navBtn:pressed[primary='true'] { - background-color: #3049F4; - } - )"); -} - -void Setup::selectLanguage() { - QMap langs = getSupportedLanguages(); - QString selection = MultiOptionDialog::getSelection(tr("Select a language"), langs.keys(), "", this); - if (!selection.isEmpty()) { - QString selectedLang = langs[selection]; - Params().put("LanguageSetting", selectedLang.toStdString()); - if (translator.load(":/" + selectedLang)) { - qApp->installTranslator(&translator); - } - } -} - -int main(int argc, char *argv[]) { - QApplication a(argc, argv); - Setup setup; - setMainWindow(&setup); - return a.exec(); -} diff --git a/selfdrive/ui/qt/setup/setup.h b/selfdrive/ui/qt/setup/setup.h deleted file mode 100644 index 986956c902..0000000000 --- a/selfdrive/ui/qt/setup/setup.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -class Setup : public QStackedWidget { - Q_OBJECT - -public: - explicit Setup(QWidget *parent = 0); - -private: - void selectLanguage(); - QWidget *low_voltage(); - QWidget *custom_software_warning(); - QWidget *getting_started(); - QWidget *network_setup(); - QWidget *software_selection(); - QWidget *downloading(); - QWidget *download_failed(QLabel *url, QLabel *body); - - QWidget *failed_widget; - QWidget *downloading_widget; - QWidget *custom_software_warning_widget; - QWidget *software_selection_widget; - QTranslator translator; - -signals: - void finished(const QString &url, const QString &error = ""); - -public slots: - void nextPage(); - void prevPage(); - void download(QString url); -}; diff --git a/selfdrive/ui/qt/setup/updater.cc b/selfdrive/ui/qt/setup/updater.cc deleted file mode 100644 index ed47590aa3..0000000000 --- a/selfdrive/ui/qt/setup/updater.cc +++ /dev/null @@ -1,186 +0,0 @@ -#include -#include -#include - -#include "system/hardware/hw.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/setup/updater.h" -#include "selfdrive/ui/qt/network/networking.h" - -Updater::Updater(const QString &updater_path, const QString &manifest_path, QWidget *parent) - : updater(updater_path), manifest(manifest_path), QStackedWidget(parent) { - - assert(updater.size()); - assert(manifest.size()); - - // initial prompt screen - prompt = new QWidget; - { - QVBoxLayout *layout = new QVBoxLayout(prompt); - layout->setContentsMargins(100, 250, 100, 100); - - QLabel *title = new QLabel(tr("Update Required")); - title->setStyleSheet("font-size: 80px; font-weight: bold;"); - layout->addWidget(title); - - layout->addSpacing(75); - - QLabel *desc = new QLabel(tr("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB.")); - desc->setWordWrap(true); - desc->setStyleSheet("font-size: 65px;"); - layout->addWidget(desc); - - layout->addStretch(); - - QHBoxLayout *hlayout = new QHBoxLayout; - hlayout->setSpacing(30); - layout->addLayout(hlayout); - - QPushButton *connect = new QPushButton(tr("Connect to Wi-Fi")); - connect->setObjectName("navBtn"); - QObject::connect(connect, &QPushButton::clicked, [=]() { - setCurrentWidget(wifi); - }); - hlayout->addWidget(connect); - - QPushButton *install = new QPushButton(tr("Install")); - install->setObjectName("navBtn"); - install->setStyleSheet(R"( - QPushButton { - background-color: #465BEA; - } - QPushButton:pressed { - background-color: #3049F4; - } - )"); - QObject::connect(install, &QPushButton::clicked, this, &Updater::installUpdate); - hlayout->addWidget(install); - } - - // wifi connection screen - wifi = new QWidget; - { - QVBoxLayout *layout = new QVBoxLayout(wifi); - layout->setContentsMargins(100, 100, 100, 100); - - Networking *networking = new Networking(this, false); - networking->setStyleSheet("Networking { background-color: #292929; border-radius: 13px; }"); - layout->addWidget(networking, 1); - - QPushButton *back = new QPushButton(tr("Back")); - back->setObjectName("navBtn"); - back->setStyleSheet("padding-left: 60px; padding-right: 60px;"); - QObject::connect(back, &QPushButton::clicked, [=]() { - setCurrentWidget(prompt); - }); - layout->addWidget(back, 0, Qt::AlignLeft); - } - - // progress screen - progress = new QWidget; - { - QVBoxLayout *layout = new QVBoxLayout(progress); - layout->setContentsMargins(150, 330, 150, 150); - layout->setSpacing(0); - - text = new QLabel(tr("Loading...")); - text->setStyleSheet("font-size: 90px; font-weight: 600;"); - layout->addWidget(text, 0, Qt::AlignTop); - - layout->addSpacing(100); - - bar = new QProgressBar(); - bar->setRange(0, 100); - bar->setTextVisible(false); - bar->setFixedHeight(72); - layout->addWidget(bar, 0, Qt::AlignTop); - - layout->addStretch(); - - reboot = new QPushButton(tr("Reboot")); - reboot->setObjectName("navBtn"); - reboot->setStyleSheet("padding-left: 60px; padding-right: 60px;"); - QObject::connect(reboot, &QPushButton::clicked, [=]() { - Hardware::reboot(); - }); - layout->addWidget(reboot, 0, Qt::AlignLeft); - reboot->hide(); - - layout->addStretch(); - } - - addWidget(prompt); - addWidget(wifi); - addWidget(progress); - - setStyleSheet(R"( - * { - color: white; - outline: none; - font-family: Inter; - } - Updater { - color: white; - background-color: black; - } - QPushButton#navBtn { - height: 160; - font-size: 55px; - font-weight: 400; - border-radius: 10px; - background-color: #333333; - } - QPushButton#navBtn:pressed { - background-color: #444444; - } - QProgressBar { - border: none; - background-color: #292929; - } - QProgressBar::chunk { - background-color: #364DEF; - } - )"); -} - -void Updater::installUpdate() { - setCurrentWidget(progress); - QObject::connect(&proc, &QProcess::readyReadStandardOutput, this, &Updater::readProgress); - QObject::connect(&proc, QOverload::of(&QProcess::finished), this, &Updater::updateFinished); - proc.setProcessChannelMode(QProcess::ForwardedErrorChannel); - proc.start(updater, {"--swap", manifest}); -} - -void Updater::readProgress() { - auto lines = QString(proc.readAllStandardOutput()); - for (const QString &line : lines.trimmed().split("\n")) { - auto parts = line.split(":"); - if (parts.size() == 2) { - text->setText(parts[0]); - bar->setValue((int)parts[1].toDouble()); - } else { - qDebug() << line; - } - } - update(); -} - -void Updater::updateFinished(int exitCode, QProcess::ExitStatus exitStatus) { - qDebug() << "finished with " << exitCode; - if (exitCode == 0) { - Hardware::reboot(); - } else { - text->setText(tr("Update failed")); - reboot->show(); - } -} - -int main(int argc, char *argv[]) { - initApp(argc, argv); - QApplication a(argc, argv); - Updater updater(argv[1], argv[2]); - setMainWindow(&updater); - a.installEventFilter(&updater); - return a.exec(); -} diff --git a/selfdrive/ui/qt/setup/updater.h b/selfdrive/ui/qt/setup/updater.h deleted file mode 100644 index ce46c0aabd..0000000000 --- a/selfdrive/ui/qt/setup/updater.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -class Updater : public QStackedWidget { - Q_OBJECT - -public: - explicit Updater(const QString &updater_path, const QString &manifest_path, QWidget *parent = 0); - -private slots: - void installUpdate(); - void readProgress(); - void updateFinished(int exitCode, QProcess::ExitStatus exitStatus); - -private: - QProcess proc; - QString updater, manifest; - - QLabel *text; - QProgressBar *bar; - QPushButton *reboot; - QWidget *prompt, *wifi, *progress; -}; diff --git a/selfdrive/ui/qt/sidebar.cc b/selfdrive/ui/qt/sidebar.cc index c63cc01592..d7e5e6c9dc 100644 --- a/selfdrive/ui/qt/sidebar.cc +++ b/selfdrive/ui/qt/sidebar.cc @@ -38,7 +38,7 @@ Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed( QObject::connect(uiState(), &UIState::uiUpdate, this, &Sidebar::updateState); - pm = std::make_unique(std::vector{"userFlag"}); + pm = std::make_unique(std::vector{"bookmarkButton"}); } void Sidebar::mousePressEvent(QMouseEvent *event) { @@ -61,8 +61,8 @@ void Sidebar::mouseReleaseEvent(QMouseEvent *event) { } if (onroad && home_btn.contains(event->pos())) { MessageBuilder msg; - msg.initEvent().initUserFlag(); - pm->send("userFlag", msg); + msg.initEvent().initBookmarkButton(); + pm->send("bookmarkButton", msg); } else if (settings_btn.contains(event->pos())) { emit openSettings(); } else if (recording_audio && mic_indicator_btn.contains(event->pos())) { diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc index dedc59f0e4..9af76e4aea 100644 --- a/selfdrive/ui/qt/util.cc +++ b/selfdrive/ui/qt/util.cc @@ -18,6 +18,7 @@ #include #include "common/swaglog.h" +#include "common/util.h" #include "system/hardware/hw.h" QString getVersion() { @@ -57,6 +58,10 @@ QMap getSupportedLanguages() { } QString timeAgo(const QDateTime &date) { + if (!util::system_time_valid()) { + return date.date().toString(); + } + int diff = date.secsTo(QDateTime::currentDateTimeUtc()); QString s; diff --git a/selfdrive/ui/qt/widgets/offroad_alerts.cc b/selfdrive/ui/qt/widgets/offroad_alerts.cc index f630875978..3a4828a2a8 100644 --- a/selfdrive/ui/qt/widgets/offroad_alerts.cc +++ b/selfdrive/ui/qt/widgets/offroad_alerts.cc @@ -32,15 +32,19 @@ AbstractAlert::AbstractAlert(bool hasRebootBtn, QWidget *parent) : QFrame(parent footer_layout->addWidget(dismiss_btn, 0, Qt::AlignBottom | Qt::AlignLeft); QObject::connect(dismiss_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss); - snooze_btn = new QPushButton(tr("Snooze Update")); - snooze_btn->setVisible(false); - snooze_btn->setFixedSize(550, 125); - footer_layout->addWidget(snooze_btn, 0, Qt::AlignBottom | Qt::AlignRight); - QObject::connect(snooze_btn, &QPushButton::clicked, [=]() { - params.putBool("SnoozeUpdate", true); + action_btn = new QPushButton(); + action_btn->setVisible(false); + action_btn->setFixedHeight(125); + footer_layout->addWidget(action_btn, 0, Qt::AlignBottom | Qt::AlignRight); + QObject::connect(action_btn, &QPushButton::clicked, [=]() { + if (!alerts["Offroad_ExcessiveActuation"]->text().isEmpty()) { + params.remove("Offroad_ExcessiveActuation"); + } else { + params.putBool("SnoozeUpdate", true); + } }); - QObject::connect(snooze_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss); - snooze_btn->setStyleSheet(R"(color: white; background-color: #4F4F4F;)"); + QObject::connect(action_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss); + action_btn->setStyleSheet("color: white; background-color: #4F4F4F; padding-left: 60px; padding-right: 60px;"); if (hasRebootBtn) { QPushButton *rebootBtn = new QPushButton(tr("Reboot and Update")); @@ -107,7 +111,14 @@ int OffroadAlert::refresh() { label->setVisible(!text.isEmpty()); alertCount += !text.isEmpty(); } - snooze_btn->setVisible(!alerts["Offroad_ConnectivityNeeded"]->text().isEmpty()); + + action_btn->setVisible(!alerts["Offroad_ExcessiveActuation"]->text().isEmpty() || !alerts["Offroad_ConnectivityNeeded"]->text().isEmpty()); + if (!alerts["Offroad_ExcessiveActuation"]->text().isEmpty()) { + action_btn->setText(tr("Acknowledge Excessive Actuation")); + } else { + action_btn->setText(tr("Snooze Update")); + } + return alertCount; } diff --git a/selfdrive/ui/qt/widgets/offroad_alerts.h b/selfdrive/ui/qt/widgets/offroad_alerts.h index ace2e75456..2dcf4f9d8c 100644 --- a/selfdrive/ui/qt/widgets/offroad_alerts.h +++ b/selfdrive/ui/qt/widgets/offroad_alerts.h @@ -15,9 +15,10 @@ class AbstractAlert : public QFrame { protected: AbstractAlert(bool hasRebootBtn, QWidget *parent = nullptr); - QPushButton *snooze_btn; + QPushButton *action_btn; QVBoxLayout *scrollable_layout; Params params; + std::map alerts; signals: void dismiss(); @@ -40,7 +41,4 @@ class OffroadAlert : public AbstractAlert { public: explicit OffroadAlert(QWidget *parent = 0) : AbstractAlert(false, parent) {} int refresh(); - -private: - std::map alerts; }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc index 23d4c4f87b..0699deef0d 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc @@ -102,13 +102,13 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { list->addItem(lagd_toggle_control); // Software delay control - delay_control = new OptionControlSP("LagdToggledelay", tr("Adjust Software Delay"), + delay_control = new OptionControlSP("LagdToggleDelay", tr("Adjust Software Delay"), tr("Adjust the software delay when Live Learning Steer Delay is toggled off." "\nThe default software delay value is 0.2"), "", {5, 30}, 1, false, nullptr, true, true); connect(delay_control, &OptionControlSP::updateLabels, [=]() { - float value = QString::fromStdString(params.get("LagdToggledelay")).toFloat(); + float value = QString::fromStdString(params.get("LagdToggleDelay")).toFloat(); delay_control->setLabel(QString::number(value, 'f', 2) + "s"); }); connect(lagd_toggle_control, &ParamControlSP::toggleFlipped, [=](bool state) { @@ -361,15 +361,11 @@ void ModelsPanel::updateLabels() { QString desc = tr("Enable this for the car to learn and adapt its steering response time. " "Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. " "The Current value is updated automatically when the vehicle is Onroad."); - QString current = QString::fromStdString(params.get("LagdToggleDesc", false)); - if (!current.isEmpty()) { - desc += "

" + tr("Current:") + " " + current + ""; - } lagd_toggle_control->setDescription(desc); delay_control->setVisible(!params.getBool("LagdToggle")); if (delay_control->isVisible()) { - float value = QString::fromStdString(params.get("LagdToggledelay")).toFloat(); + float value = QString::fromStdString(params.get("LagdToggleDelay")).toFloat(); delay_control->setLabel(QString::number(value, 'f', 2) + "s"); } diff --git a/selfdrive/ui/tests/test_feedbackd.py b/selfdrive/ui/tests/test_feedbackd.py new file mode 100644 index 0000000000..c2d81aef83 --- /dev/null +++ b/selfdrive/ui/tests/test_feedbackd.py @@ -0,0 +1,52 @@ +import pytest +import cereal.messaging as messaging +from cereal import car +from openpilot.common.params import Params +from openpilot.system.manager.process_config import managed_processes + + +class TestFeedbackd: + def setup_method(self): + self.pm = messaging.PubMaster(['carState', 'rawAudioData']) + self.sm = messaging.SubMaster(['audioFeedback']) + + def _send_lkas_button(self, pressed: bool): + msg = messaging.new_message('carState') + msg.carState.canValid = True + msg.carState.buttonEvents = [{'type': car.CarState.ButtonEvent.Type.lkas, 'pressed': pressed}] + self.pm.send('carState', msg) + + def _send_audio_data(self, count: int = 5): + for _ in range(count): + audio_msg = messaging.new_message('rawAudioData') + audio_msg.rawAudioData.data = bytes(1600) # 800 samples of int16 + audio_msg.rawAudioData.sampleRate = 16000 + self.pm.send('rawAudioData', audio_msg) + self.sm.update(timeout=100) + + @pytest.mark.parametrize("record_feedback", [False, True]) + def test_audio_feedback(self, record_feedback): + Params().put_bool("RecordAudioFeedback", record_feedback) + + managed_processes["feedbackd"].start() + assert self.pm.wait_for_readers_to_update('carState', timeout=5) + assert self.pm.wait_for_readers_to_update('rawAudioData', timeout=5) + + self._send_lkas_button(pressed=True) + self._send_audio_data() + self._send_lkas_button(pressed=False) + self._send_audio_data() + + if record_feedback: + assert self.sm.updated['audioFeedback'], "audioFeedback should be published when enabled" + else: + assert not self.sm.updated['audioFeedback'], "audioFeedback should not be published when disabled" + + self._send_lkas_button(pressed=True) + self._send_audio_data() + self._send_lkas_button(pressed=False) + self._send_audio_data() + + assert not self.sm.updated['audioFeedback'], "audioFeedback should not be published after second press" + + managed_processes["feedbackd"].stop() diff --git a/selfdrive/ui/tests/test_raylib_ui.py b/selfdrive/ui/tests/test_raylib_ui.py new file mode 100644 index 0000000000..3f23301972 --- /dev/null +++ b/selfdrive/ui/tests/test_raylib_ui.py @@ -0,0 +1,8 @@ +import time +from openpilot.selfdrive.test.helpers import with_processes + + +@with_processes(["raylib_ui"]) +def test_raylib_ui(): + """Test initialization of the UI widgets is successful.""" + time.sleep(1) diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index b263e28297..653395ba1d 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import capnp -import json import pathlib import shutil import sys @@ -265,12 +264,12 @@ def setup_settings_trips(click, pm: PubMaster, scroll=None): time.sleep(UI_DELAY) def setup_settings_vehicle(click, pm: PubMaster, scroll=None): - Params().put("CarPlatformBundle", json.dumps( + Params().put("CarPlatformBundle", { "platform": "HONDA_CIVIC_2022", "name": "Honda Civic 2022-24" } - )) + ) setup_settings_device(click, pm) scroll(-400, 278, 962) @@ -408,9 +407,9 @@ def create_screenshots(): params.put("DongleId", "123456789012345") params.put("SunnylinkDongleId", "123456789012345") if name == 'prime': - params.put('PrimeType', '1') + params.put('PrimeType', 1) elif name == 'pair_device': - params.put('ApiCache_Device', '{"is_paired":0, "prime_type":-1}') + params.put('ApiCache_Device', {"is_paired":0, "prime_type":-1}) t.test_ui(name, setup) diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 61eb36f4c3..95fd0ccbb4 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -7,10 +7,6 @@ Close إغلاق - - Snooze Update - تأخير التحديث - Reboot and Update إعادة التشغيل والتحديث @@ -167,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - يجب عليك قبول الشروط والأحكام من أجل استخدام sunnypilot. - Back السابق @@ -179,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 رفض، إلغاء التثبيت %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -190,6 +186,14 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode وضع المناورة الطولية + + openpilot Longitudinal Control (Alpha) + التحكم الطولي openpilot (ألفا) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + تحذير: التحكم الطولي في openpilot في المرحلة ألفا لهذه السيارة، وسيقوم بتعطيل مكابح الطوارئ الآلية (AEB). + Enable ADB تمكين ADB @@ -198,14 +202,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. أداة ADB (Android Debug Bridge) تسمح بالاتصال بجهازك عبر USB أو عبر الشبكة. راجع هذا الرابط: https://docs.comma.ai/how-to/connect-to-comma لمزيد من المعلومات. - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. @@ -301,10 +297,6 @@ This only toggles the visibility of the controls; it does not toggle the actual REVIEW مراجعة - - Review the rules, features, and limitations of sunnypilot - مراجعة الأدوار والميزات والقيود في sunnypilot - Are you sure you want to review the training guide? هل أنت متأكد أنك تريد مراجعة دليل التدريب؟ @@ -398,7 +390,7 @@ This only toggles the visibility of the controls; it does not toggle the actual - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. @@ -422,12 +414,20 @@ Steering lag calibration is complete. - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + Review the rules, features, and limitations of sunnypilot + + + + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -444,6 +444,10 @@ Steering lag calibration is complete. Language + + Reset Settings + + Are you sure you want to review the training guide? هل أنت متأكد أنك تريد مراجعة دليل التدريب؟ @@ -456,6 +460,19 @@ Steering lag calibration is complete. Select a language اختر لغة + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot إعادة التشغيل @@ -474,7 +491,7 @@ Steering lag calibration is complete. Confirm - تأكيد + Are you sure you want to enter Always Offroad mode? @@ -484,22 +501,6 @@ Steering lag calibration is complete. Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -513,16 +514,11 @@ Steering lag calibration is complete. - Wake-Up Behavior + Exit Always Offroad - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. + Always Offroad @@ -580,7 +576,7 @@ This is the time after which settings UI closes automatically if user is not int Confirm - تأكيد + EXIT ALWAYS OFFROAD MODE @@ -608,10 +604,6 @@ This is the time after which settings UI closes automatically if user is not int ACTIVE نشط - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - للحصول على أقصى فعالية، أحضر جهازك إلى الداخل واتصل بمحول USB-C جيد وشبكة Wi-Fi أسبوعياً.<br><br>يمكن أن يعمل وضع خرطوم الحريق أيضاً أثناء القيادة إذا كنت متصلاً بنقطة اتصال أو ببطاقة SIM غير محدودة.<br><br><br><b>الأسئلة المتكررة</b><br><br><i>هل يهم كيف أو أين أقود؟</i> لا، فقط قد كما تفعل عادة.<br><br><i>هل يتم سحب كل مقاطع رحلاتي في وضع خرطوم الحريق؟</i> لا، نقوم بسحب مجموعة مختارة من مقاطع رحلاتك.<br><br><i>ما هو محول USB-C الجيد؟</i> أي شاحن سريع للهاتف أو اللابتوب يجب أن يكون مناسباً.<br><br><i>هل يهم أي برنامج أستخدم؟</i> نعم، فقط النسخة الأصلية من sunnypilot (وأفرع معينة) يمكن استخدامها للتدريب. - <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -623,6 +615,14 @@ This is the time after which settings UI closes automatically if user is not int حتى الآن، يوجد </b>%n مقطع<b>%n من قيادتك في مجموعة بيانات التدريب. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + + + + Firehose Mode + + sunnypilot learns to drive by watching humans, like you, drive. @@ -630,11 +630,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - - - - Firehose Mode + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -747,6 +743,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -763,14 +767,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform supports limited MADS settings. - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - LongitudinalPanel @@ -802,11 +798,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -817,18 +817,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -845,6 +833,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -857,6 +849,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -903,66 +899,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECT اختيار - - downloaded - - - - ready - - - - from cache - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - إعادة ضبط المعايرة - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - متابعة - - - on Metered - - - - Cancel - إلغاء - Clear Model Cache @@ -1004,6 +940,18 @@ The default software delay value is 0.2 %1 - %2 + + downloaded + + + + ready + + + + from cache + + download failed - %1 @@ -1012,12 +960,40 @@ The default software delay value is 0.2 pending - %1 + + Fetching models... + + + + Select a Model + + + + Default + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad. - Current: + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + إعادة ضبط المعايرة + + + Driving Model Selector @@ -1028,6 +1004,22 @@ The default software delay value is 0.2 Clear Cache + + Warning: You are on a metered connection! + + + + Continue + متابعة + + + on Metered + + + + Cancel + إلغاء + MultiOptionDialog @@ -1135,14 +1127,6 @@ The default software delay value is 0.2 Device temperature too high. System cooling down before starting. Current internal component temperature: %1 درجة حرارة الجهاز مرتفعة جداً. يقوم النظام بالتبريد قبل البدء. درجة الحرارة الحالية للمكونات الداخلية: %1 - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - اتصل فوراً بالإنترنت للتحقق من وجود تحديثات. إذا لم تكم متصلاً بالإنترنت فإن sunnypilot لن يساهم في %1 - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - اتصل بالإنترنت للتحقق من وجود تحديثات. لا يعمل sunnypilot تلقائياً إلا إذا اتصل بالإنترنت من أجل التحقق من التحديثات. - Unable to download updates %1 @@ -1157,25 +1141,41 @@ The default software delay value is 0.2 An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. يتم تنزيل تحديث لنظام تشغيل جهازك في الخلفية. سيطلَب منك التحديث عندما يصبح جاهزاً للتثبيت. - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - فشل تسجيل الجهاز. لن يقوم بالاتصال أو تحميل خوادم comma.ai، ولا تلقي الدعم من comma.ai. إذا كان هذا الجهاز نظامياً فيرجى زيارة الموقع https://comma.ai/support. - NVMe drive not mounted. محرك NVMe غير مثبَّت. - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - تم اكتشاف محرك NVMe غير مدعوم. قد يستهلك الجهاز قدراً أكبر بكثير من الطاقة، وزيادة في ارتفاع درجة الحرارة بسبب وجود NVMe غير مدعوم. + Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. + + + + Acknowledge Excessive Actuation + + + + Snooze Update + تأخير التحديث + + + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - لم يكن sunnypilot قادراً على تحديد سيارتك. إما أن تكون سيارتك غير مدعومة أو أنه لم يتم التعرف على وحدة التحكم الإلكتروني (ECUs) فيها. يرجى تقديم طلب سحب من أجل إضافة نسخ برمجيات ثابتة إلى السيارة المناسبة. هل تحتاج إلى أي مساعدة؟ لا تتردد في التواصل مع doscord.comma.ai. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - لقد اكتشف sunnypilot تغييراً في موقع تركيب الجهاز. تأكد من تثبيت الجهاز بشكل كامل في موقعه وتثبيته بإحكام على الزجاج الأمامي. + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. @@ -1208,10 +1208,6 @@ The default software delay value is 0.2 OnroadAlerts - - sunnypilot Unavailable - sunnypilot غير متوفر - TAKE CONTROL IMMEDIATELY تحكم على الفور @@ -1228,6 +1224,10 @@ The default software delay value is 0.2 System Unresponsive النظام لا يستجيب + + sunnypilot Unavailable + + OsmPanel @@ -1445,7 +1445,7 @@ Warning: You are on a metered connection! Confirm - تأكيد + Cancel @@ -1528,10 +1528,6 @@ Warning: You are on a metered connection! QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1569,46 +1565,9 @@ Warning: You are on a metered connection! now الآن - - - Reset - Reset failed. Reboot to try again. - فشل إعاة الضبط. أعد التشغيل للمحاولة من جديد. - - - Are you sure you want to reset your device? - هل أنت متأكد أنك تريد إعادة ضبط جهازك؟ - - - System Reset - إعادة ضبط النظام - - - Cancel - إلغاء - - - Reboot - إعادة التشغيل - - - Confirm - تأكيد - - - Resetting device... -This may take up to a minute. - يتم إعادة ضبط الجهاز... -قد يستغرق الأمر حوالي الدقيقة. - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - غير قادر على تحميل جزء البيانات. قد يكون الجزء تالفاً. اضغط على تأكيد لمسح جهازك وإعادة ضبطه. - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - تم تفعيل إعادة ضبط النظام. اضغط على تأكيد لمسح جميع المحتويات والإعدادات. اضغط على إلغاء لاستئناف التمهيد. + sunnypilot + @@ -1669,29 +1628,13 @@ This may take up to a minute. البرنامج - Trips + Models - - Vehicle - - - - Developer - المطور - - - Firehose - خرطوم الحريق - Steering - - Models - - Cruise @@ -1704,111 +1647,22 @@ This may take up to a minute. OSM - - - Setup - WARNING: Low Voltage - تحذير: الجهد منخفض - - - Power your device in a car with a harness or proceed at your own risk. - شغل جهازك في السيارة عن طريق شرطان التوصيل، أو تابع على مسؤوليتك. - - - Power off - إيقاف التشغيل - - - Continue - متابعة - - - Getting Started - البدء - - - Before we get on the road, let’s finish installation and cover some details. - قبل أن ننطلق في الطريق، دعنا ننتهي من التثبيت ونغطي بعض التفاصيل. - - - Connect to Wi-Fi - الاتصال بشبكة الواي فاي - - - Back - السابق - - - Continue without Wi-Fi - المتابعة بدون شبكة الواي فاي - - - Waiting for internet - بانتظار الاتصال بالإنترنت - - - Enter URL - أدخل رابط URL - - - for Custom Software - للبرامج المخصصة - - - Downloading... - يتم الآن التنزيل... - - - Download Failed - فشل التنزيل - - - Ensure the entered URL is valid, and the device’s internet connection is good. - تأكد من أن رابط URL الذي أدخلته صالح، وأن اتصال الجهاز بالإنترنت جيد. - - - Reboot device - إعادة التشغيل - - - Start over - البدء من جديد - - - Something went wrong. Reboot the device. - حدث خطأ ما. أعد التشغيل الجهاز. - - - No custom software found at this URL. - لم يتم العثور على برنامج خاص لعنوان URL ها. - - - Select a language - اختر لغة - - - Choose Software to Install - اختر البرنامج للتثبيت - - - sunnypilot - sunnypilot - - - Custom Software - البرمجيات المخصصة - - - WARNING: Custom Software + Trips - Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. - -If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + Vehicle + + Firehose + خرطوم الحريق + + + Developer + المطور + SetupWidget @@ -2012,14 +1866,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enter search keywords, or leave blank to list all branches. - - No branches found for keywords: %1 - - - - Select a branch - اختر فرعاً - Disable Updates @@ -2028,6 +1874,14 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 + + + + Select a branch + اختر فرعاً + %1 updates requires a reboot.<br>Reboot now? @@ -2101,22 +1955,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - غير متاح - Sponsor Status @@ -2141,24 +1979,20 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + غير متاح + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -2213,6 +2047,26 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -2265,11 +2119,11 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Welcome to sunnypilot - مرحباً بكم في sunnypilot + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - يجب عليك قبول الشروط والأحكام لاستخدام sunnypilot. اقرأ أحدث الشروط على <span style='color: #465BEA;'>https://comma.ai/terms</span> قبل الاستمرار. + @@ -2302,10 +2156,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Disengage on Accelerator Pedal فك الارتباط عن دواسة الوقود - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - عند تمكين هذه الميزة، فإن الضغط على دواسة الوقود سيؤدي إلى فك ارتباط sunnypilot. - Experimental Mode الوضع التجريبي @@ -2326,18 +2176,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Driving Personality شخصية القيادة - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - يتم وضع sunnypilot بشكل قياسي في <b>وضعية الراحة</b>. يمكن الوضع التجريبي <b>ميزات المستوى ألفا</b> التي لا تكون جاهزة في وضع الراحة: - End-to-End Longitudinal Control التحكم الطولي من طرف إلى طرف - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - دع نظام القيادة يتحكم بالوقود والمكابح. سيقوم sunnypilot بالقيادة كما لو أنه كائن بشري، بما في ذلك التوقف عند الإشارة الحمراء، وإشارات التوقف. وبما أن نمط القيادة يحدد سرعة القيادة، فإن السرعة المضبوطة تشكل الحد الأقصى فقط. هذه خاصية الجودة ألفا، فيجب توقع حدوث الأخطاء. - New Driving Visualization تصور القيادة الديد @@ -2347,16 +2189,8 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device الوضع التجريبي غير متوفر حالياً في هذه السيارة نظراً لاستخدام رصيد التحكم التكيفي بالسرعة من أجل التحكم الطولي. - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - يمكن اختبار نسخة ألفا من التحكم الطولي من sunnypilot، مع الوضع التجريبي، لكن على الفروع غير المطلقة. - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - تمكين التحكم الطولي من sunnypilot (ألفا) للسماح بالوضع التجريبي. - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - يوصى بالمعيار. في الوضع العدواني، سيتبع الطيار المفتوح السيارات الرائدة بشكل أقرب ويكون أكثر عدوانية مع البنزين والفرامل. في الوضع المريح، سيبقى sunnypilot بعيدًا عن السيارات الرائدة. في السيارات المدعومة، يمكنك التنقل بين هذه الشخصيات باستخدام زر مسافة عجلة القيادة. + openpilot longitudinal control may come in a future update. + قد يتم الحصول على التحكم الطولي في openpilot في عمليات التحديث المستقبلية. The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. @@ -2367,8 +2201,34 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device مراقبة السائق المستمرة - Enable driver monitoring even when sunnypilot is not engaged. - تمكين مراقبة السائق حتى عندما لا يكون نظام OpenPilot مُفعّلاً. + Changing this setting will restart openpilot if the car is powered on. + + + + Record and Upload Microphone Audio + + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2379,27 +2239,31 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - Enable sunnypilot - تمكين sunnypilot - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Record and Upload Microphone Audio + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. @@ -2414,41 +2278,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device إلغاء - - Updater - - Update Required - التحديث مطلوب - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - تحديث نظام التشغيل مطلوب. قم بوصل جهازك بشبكة واي فاي من أجل تحديث أسرع. حجم التحميل حوالي 1 غيغا بايت تقريباً. - - - Connect to Wi-Fi - الاتصال بشبكة الواي فاي - - - Install - تثبيت - - - Back - السابق - - - Loading... - يتم التحميل... - - - Reboot - إعادة التشغيل - - - Update failed - فشل التحديث - - VisualsPanel @@ -2499,12 +2328,12 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device انفتح - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> وضع خرطوم الحريق <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. + قم بزيادة تحميلات بيانات التدريب الخاصة بك لتحسين نماذج القيادة الخاصة بـ openpilot. - Maximize your training data uploads to improve openpilot's driving models. - + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> وضع خرطوم الحريق <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index dbde9c280a..1a928b4e49 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -7,10 +7,6 @@ Close Schließen - - Snooze Update - Update pausieren - Reboot and Update Aktualisieren und neu starten @@ -394,11 +390,7 @@ This only toggles the visibility of the controls; it does not toggle the actual - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. @@ -422,7 +414,11 @@ Steering lag calibration is complete. - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + Review the rules, features, and limitations of sunnypilot + + + + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. @@ -464,6 +460,19 @@ Steering lag calibration is complete. Select a language Sprache wählen + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot Neustart @@ -482,7 +491,7 @@ Steering lag calibration is complete. Confirm - Bestätigen + Are you sure you want to enter Always Offroad mode? @@ -512,19 +521,6 @@ Steering lag calibration is complete. Always Offroad - - Wake-Up Behavior - - - - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. - - ⁍ Default: Device will boot/wake-up normally & will be ready to engage. @@ -580,7 +576,7 @@ This is the time after which settings UI closes automatically if user is not int Confirm - Bestätigen + EXIT ALWAYS OFFROAD MODE @@ -619,6 +615,10 @@ This is the time after which settings UI closes automatically if user is not int <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INAKTIV</span>: Verbinde dich mit einem ungedrosselten Netzwerk + + Firehose Mode + + sunnypilot learns to drive by watching humans, like you, drive. @@ -629,10 +629,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - Firehose Mode - - HudRenderer @@ -739,6 +735,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -755,14 +759,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform supports limited MADS settings. - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - LongitudinalPanel @@ -895,66 +891,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECT AUSWÄHLEN - - downloaded - - - - ready - - - - from cache - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Neu kalibrieren - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - Fortsetzen - - - on Metered - - - - Cancel - Abbrechen - Clear Model Cache @@ -996,6 +932,18 @@ The default software delay value is 0.2 %1 - %2 + + downloaded + + + + ready + + + + from cache + + download failed - %1 @@ -1004,12 +952,40 @@ The default software delay value is 0.2 pending - %1 + + Fetching models... + + + + Select a Model + + + + Default + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad. - Current: + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + Neu kalibrieren + + + Driving Model Selector @@ -1020,6 +996,22 @@ The default software delay value is 0.2 Clear Cache + + Warning: You are on a metered connection! + + + + Continue + Fortsetzen + + + on Metered + + + + Cancel + Abbrechen + MultiOptionDialog @@ -1137,22 +1129,30 @@ The default software delay value is 0.2 An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. Ein Update für das Betriebssystem deines Geräts wird im Hintergrund heruntergeladen. Du wirst aufgefordert, das Update zu installieren, sobald es bereit ist. - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - Gerät konnte nicht registriert werden. Es wird keine Verbindung zu den comma.ai-Servern herstellen oder Daten hochladen und erhält keinen Support von comma.ai. Wenn dies ein offizielles Gerät ist, besuche https://comma.ai/support. - NVMe drive not mounted. NVMe-Laufwerk nicht gemounted. - - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - Nicht unterstütztes NVMe-Laufwerk erkannt. Das Gerät kann dadurch deutlich mehr Strom verbrauchen und überhitzen. - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 Gerätetemperatur zu hoch. Das System kühlt ab, bevor es startet. Aktuelle interne Komponententemperatur: %1 + + Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. + + + + Acknowledge Excessive Actuation + + + + Snooze Update + Update pausieren + + + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 @@ -1437,7 +1437,7 @@ Warning: You are on a metered connection! Confirm - Bestätigen + Cancel @@ -1550,47 +1550,6 @@ Warning: You are on a metered connection! - - Reset - - Reset failed. Reboot to try again. - Zurücksetzen fehlgeschlagen. Starte das Gerät neu und versuche es wieder. - - - Are you sure you want to reset your device? - Bist du sicher, dass du das Gerät auf Werkseinstellungen zurücksetzen möchtest? - - - System Reset - System auf Werkseinstellungen zurücksetzen - - - Cancel - Abbrechen - - - Reboot - Neustart - - - Confirm - Bestätigen - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - Datenpartition konnte nicht gemounted werden. Die Partition ist möglicherweise beschädigt. Drücke Bestätigen, um das Gerät zu löschen und zurückzusetzen. - - - Resetting device... -This may take up to a minute. - Gerät wird zurückgesetzt... -Dies kann bis zu einer Minute dauern. - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - System-Reset ausgelöst. Drücke Bestätigen, um alle Inhalte und Einstellungen zu löschen. Drücke Abbrechen, um den Startvorgang fortzusetzen. - - SettingsWindow @@ -1660,6 +1619,14 @@ Dies kann bis zu einer Minute dauern. Cruise + + Visuals + + + + OSM + + Trips @@ -1676,119 +1643,6 @@ Dies kann bis zu einer Minute dauern. Developer Entwickler - - Visuals - - - - OSM - - - - - Setup - - WARNING: Low Voltage - Warnung: Batteriespannung niedrig - - - Power your device in a car with a harness or proceed at your own risk. - Versorge dein Gerät über einen Kabelbaum im Auto mit Strom, oder fahre auf eigene Gefahr fort. - - - Power off - Ausschalten - - - Continue - Fortsetzen - - - Getting Started - Loslegen - - - Before we get on the road, let’s finish installation and cover some details. - Bevor wir uns auf die Straße begeben, lass uns die Installation fertigstellen und einige Details prüfen. - - - Connect to Wi-Fi - Mit WLAN verbinden - - - Back - Zurück - - - Continue without Wi-Fi - Ohne WLAN fortsetzen - - - Waiting for internet - Auf Internet warten - - - Enter URL - URL eingeben - - - for Custom Software - für spezifische Software - - - Downloading... - Herunterladen... - - - Download Failed - Herunterladen fehlgeschlagen - - - Ensure the entered URL is valid, and the device’s internet connection is good. - Stelle sicher, dass die eingegebene URL korrekt ist und dein Gerät eine stabile Internetverbindung hat. - - - Reboot device - Gerät neustarten - - - Start over - Von neuem beginnen - - - No custom software found at this URL. - Keine benutzerdefinierte Software unter dieser URL gefunden. - - - Something went wrong. Reboot the device. - Etwas ist schiefgelaufen. Starte das Gerät neu. - - - Select a language - Sprache wählen - - - Choose Software to Install - Wähle die zu installierende Software - - - Custom Software - Benutzerdefinierte Software - - - sunnypilot - - - - WARNING: Custom Software - - - - Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. - -If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. - - SetupWidget @@ -1994,14 +1848,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enter search keywords, or leave blank to list all branches. - - No branches found for keywords: %1 - - - - Select a branch - Wähle einen Branch - Disable Updates @@ -2010,6 +1856,14 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 + + + + Select a branch + Wähle einen Branch + %1 updates requires a reboot.<br>Reboot now? @@ -2328,6 +2182,28 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Always-On Driver Monitoring Dauerhaft aktive Fahrerüberwachung + + Changing this setting will restart openpilot if the car is powered on. + + + + Record and Upload Microphone Audio + + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + Enable sunnypilot @@ -2356,10 +2232,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - - Changing this setting will restart openpilot if the car is powered on. - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: @@ -2376,14 +2248,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - - Record and Upload Microphone Audio - - - - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - - TreeOptionDialog @@ -2396,41 +2260,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Abbrechen - - Updater - - Update Required - Aktualisierung notwendig - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - Eine Aktualisierung des Betriebssystems ist notwendig. Verbinde dein Gerät mit WLAN für ein schnelleres Update. Die Download Größe ist ungefähr 1GB. - - - Connect to Wi-Fi - Mit WLAN verbinden - - - Install - Installieren - - - Back - Zurück - - - Loading... - Laden... - - - Reboot - Neustart - - - Update failed - Aktualisierung fehlgeschlagen - - VisualsPanel diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index ed84ab0f42..3814373c53 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -7,10 +7,6 @@ Close Cerrar - - Snooze Update - Posponer Actualización - Reboot and Update Reiniciar y Actualizar @@ -84,27 +80,27 @@ Prevent large data uploads when on a metered cellular connection - + Evite cargas de grandes cantidades de datos cuando utilice una conexión celular medida default - + por defecto metered - + medido unmetered - + sin medidor Wi-Fi Network Metered - + Red Wi-Fi medida Prevent large data uploads when on a metered Wi-Fi connection - + Evite cargas de grandes cantidades de datos cuando esté en una conexión Wi-Fi medida @@ -167,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - Debe aceptar los términos y condiciones para poder utilizar sunnypilot. - Back Atrás @@ -179,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 Rechazar, desinstalar %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -190,21 +186,21 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode Modo de maniobra longitudinal - - Enable ADB - - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - - openpilot Longitudinal Control (Alpha) - + Control longitudinal de openpilot (fase experimental) WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - + AVISO: el control longitudinal de openpilot está en fase experimental para este automóvil y desactivará el Frenado Automático de Emergencia (AEB). + + + Enable ADB + Activar ADB + + + ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. + ADB (Android Debug Bridge) permite conectar a su dispositivo por USB o por red. Visite https://docs.comma.ai/how-to/connect-to-comma para más información. On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. @@ -317,10 +313,6 @@ This only toggles the visibility of the controls; it does not toggle the actual REVIEW REVISAR - - Review the rules, features, and limitations of sunnypilot - Revisar las reglas, características y limitaciones de sunnypilot - Are you sure you want to review the training guide? ¿Seguro que quiere revisar la guía de entrenamiento? @@ -395,39 +387,51 @@ This only toggles the visibility of the controls; it does not toggle the actual Disengage to Reset Calibration + Desactivar para restablecer la calibración + + + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + Openpilot se calibra continuamente, por lo que rara vez es necesario reiniciarlo. Restablecer la calibración reiniciará Openpilot si el automóvil está encendido. + + + + +Steering lag calibration is %1% complete. + + +La calibración del retraso de dirección está %1% completada. + + + + +Steering lag calibration is complete. + + +La calibración del retraso de la dirección está completa. + + + Steering torque response calibration is %1% complete. + La calibración de la respuesta del par de dirección está %1% completada. + + + Steering torque response calibration is complete. + La calibración de la respuesta del par de dirección está completa. + + + Review the rules, features, and limitations of sunnypilot sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - -Steering lag calibration is %1% complete. - - - - - -Steering lag calibration is complete. - - - - Steering torque response calibration is %1% complete. - - - - Steering torque response calibration is complete. - - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - - DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -444,6 +448,10 @@ Steering lag calibration is complete. Language + + Reset Settings + + Are you sure you want to review the training guide? ¿Seguro que quiere revisar la guía de entrenamiento? @@ -456,6 +464,19 @@ Steering lag calibration is complete. Select a language + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot Reiniciar @@ -474,7 +495,7 @@ Steering lag calibration is complete. Confirm - Confirmar + Are you sure you want to enter Always Offroad mode? @@ -484,22 +505,6 @@ Steering lag calibration is complete. Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -513,16 +518,11 @@ Steering lag calibration is complete. - Wake-Up Behavior + Exit Always Offroad - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. + Always Offroad @@ -580,7 +580,7 @@ This is the time after which settings UI closes automatically if user is not int Confirm - Confirmar + EXIT ALWAYS OFFROAD MODE @@ -602,23 +602,27 @@ This is the time after which settings UI closes automatically if user is not int FirehosePanel Firehose Mode: ACTIVE - + Modo Firehose: ACTIVO ACTIVE - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - + ACTIVO <b>%n segment(s)</b> of your driving is in the training dataset so far. - - - + + <b>%n segmento</b> de tu conducción está en el conjunto de datos de entrenamiento hasta ahora. + <b>%n segmentos</b> de tu conducción están en el conjunto de datos de entrenamiento hasta ahora. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVO</span>: conéctate a una red sin límite de datos + + + Firehose Mode + Modo manguera contra incendios + sunnypilot learns to drive by watching humans, like you, drive. @@ -626,11 +630,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - - - - Firehose Mode + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -739,6 +739,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -755,14 +763,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform supports limited MADS settings. - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - LongitudinalPanel @@ -794,11 +794,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -809,18 +813,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -837,6 +829,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -849,6 +845,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -895,66 +895,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECT SELECCIONAR - - downloaded - - - - ready - - - - from cache - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Formatear Calibración - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - Continuar - - - on Metered - - - - Cancel - Cancelar - Clear Model Cache @@ -996,6 +936,18 @@ The default software delay value is 0.2 %1 - %2 + + downloaded + + + + ready + + + + from cache + + download failed - %1 @@ -1004,12 +956,40 @@ The default software delay value is 0.2 pending - %1 + + Fetching models... + + + + Select a Model + + + + Default + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad. - Current: + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + Formatear Calibración + + + Driving Model Selector @@ -1020,6 +1000,22 @@ The default software delay value is 0.2 Clear Cache + + Warning: You are on a metered connection! + + + + Continue + Continuar + + + on Metered + + + + Cancel + Cancelar + MultiOptionDialog @@ -1127,14 +1123,6 @@ The default software delay value is 0.2 Device temperature too high. System cooling down before starting. Current internal component temperature: %1 La temperatura del dispositivo es muy alta. El sistema se está enfriando antes de iniciar. Temperatura actual del componente interno: %1 - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - Conéctese inmediatamente al internet para buscar actualizaciones. Si no se conecta al internet, sunnypilot no iniciará en %1 - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - Conectese al internet para buscar actualizaciones. sunnypilot no iniciará automáticamente hasta conectarse al internet para buscar actualizaciones. - Unable to download updates %1 @@ -1149,25 +1137,41 @@ The default software delay value is 0.2 An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. Se está descargando una actualización del sistema operativo de su dispositivo en segundo plano. Se le pedirá que actualice cuando esté listo para instalarse. - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - El dispositivo no pudo registrarse. No se conectará ni subirá datos a los servidores de comma.ai y no recibe soporte de comma.ai. Si este es un dispositivo oficial, visite https://comma.ai/support. - NVMe drive not mounted. Unidad NVMe no montada. - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - Se detectó una unidad NVMe incompatible. El dispositivo puede consumir mucha más energía y sobrecalentarse debido a esto. + Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. + + + + Acknowledge Excessive Actuation + + + + Snooze Update + Posponer Actualización + + + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - sunnypilot no pudo identificar su automóvil. Su automóvil no es compatible o no se reconocen sus ECU. Por favor haga un pull request para agregar las versiones de firmware del vehículo adecuado. ¿Necesita ayuda? Únase a discord.comma.ai. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilot detectó un cambio en la posición de montaje del dispositivo. Asegúrese de que el dispositivo esté completamente asentado en el soporte y que el soporte esté firmemente asegurado al parabrisas. + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. @@ -1200,10 +1204,6 @@ The default software delay value is 0.2 OnroadAlerts - - sunnypilot Unavailable - sunnypilot no disponible - TAKE CONTROL IMMEDIATELY TOME CONTROL INMEDIATAMENTE @@ -1220,6 +1220,10 @@ The default software delay value is 0.2 System Unresponsive Systema no responde + + sunnypilot Unavailable + + OsmPanel @@ -1437,7 +1441,7 @@ Warning: You are on a metered connection! Confirm - Confirmar + Cancel @@ -1520,10 +1524,6 @@ Warning: You are on a metered connection! QObject - - sunnypilot - sunnypilot - now ahora @@ -1549,46 +1549,9 @@ Warning: You are on a metered connection! hace %n días - - - Reset - Reset failed. Reboot to try again. - Formateo fallido. Reinicie para reintentar. - - - Resetting device... -This may take up to a minute. - formateando dispositivo... -Esto puede tardar un minuto. - - - Are you sure you want to reset your device? - ¿Seguro que quiere formatear su dispositivo? - - - System Reset - Formatear Sistema - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - Formateo del sistema activado. Presione confirmar para borrar todo el contenido y la configuración. Presione cancelar para reanudar el inicio. - - - Cancel - Cancelar - - - Reboot - Reiniciar - - - Confirm - Confirmar - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - No es posible montar una partición de datos. La partición podría estar corrompida. Confirme para borrar y formatear su dispositivo. + sunnypilot + @@ -1619,7 +1582,7 @@ Esto puede tardar un minuto. Firehose - + Firehose @@ -1649,29 +1612,13 @@ Esto puede tardar un minuto. Software - Trips - - - - Vehicle - - - - Developer - Desarrollador - - - Firehose + Models Steering - - Models - - Cruise @@ -1684,111 +1631,22 @@ Esto puede tardar un minuto. OSM - - - Setup - Something went wrong. Reboot the device. - Algo ha ido mal. Reinicie el dispositivo. - - - Ensure the entered URL is valid, and the device’s internet connection is good. - Asegúrese de que la URL insertada es válida y que el dispositivo tiene buena conexión. - - - No custom software found at this URL. - No encontramos software personalizado en esta URL. - - - WARNING: Low Voltage - ALERTA: Voltaje bajo - - - Power your device in a car with a harness or proceed at your own risk. - Encienda su dispositivo en un auto con el arnés o proceda bajo su propio riesgo. - - - Power off - Apagar - - - Continue - Continuar - - - Getting Started - Comenzando - - - Before we get on the road, let’s finish installation and cover some details. - Antes de ponernos en marcha, terminemos la instalación y cubramos algunos detalles. - - - Connect to Wi-Fi - Conectarse al Wi-Fi - - - Back - Volver - - - Continue without Wi-Fi - Continuar sin Wi-Fi - - - Waiting for internet - Esperando conexión a internet - - - Choose Software to Install - Elija el software a instalar - - - sunnypilot - sunnypilot - - - Custom Software - Software personalizado - - - Enter URL - Insertar URL - - - for Custom Software - para Software personalizado - - - Downloading... - Descargando... - - - Download Failed - Descarga fallida - - - Reboot device - Reiniciar Dispositivo - - - Start over - Comenzar de nuevo - - - Select a language - Seleccione un idioma - - - WARNING: Custom Software + Trips - Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. - -If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + Vehicle + + Firehose + Firehose + + + Developer + Desarrollador + SetupWidget @@ -1992,14 +1850,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enter search keywords, or leave blank to list all branches. - - No branches found for keywords: %1 - - - - Select a branch - Selecione una rama - Disable Updates @@ -2008,6 +1858,14 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 + + + + Select a branch + Selecione una rama + %1 updates requires a reboot.<br>Reboot now? @@ -2081,37 +1939,21 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - N/A - Sponsor Status - Estado de patrocinio + SPONSOR - PATROCINADOR + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - Conviértete en patrocinador de sunnypilot para obtener acceso anticipado a las funciones de sunnylink cuando estén disponibles. + Pair GitHub Account - Emparejar cuenta de GitHub + PAIR @@ -2119,27 +1961,23 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - Empareja tu cuenta de GitHub para otorgar a tu dispositivo beneficios de patrocinador, incluido acceso a la API en sunnylink. + + + + N/A + N/A sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - ID del dongle de sunnylink no encontrado. Esto puede deberse a una conexión débil o a un problema de registro en sunnylink. Por favor, reinicia y vuelve a intentarlo. + - Not Sponsor - No patrocinador + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + - Paired - Emparejado - - - Not Paired - No emparejado - - - THANKS ♥ - GRACIAS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + Backup Settings @@ -2193,44 +2031,64 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup Scan the QR code to login to your GitHub account - Escanea el código QR para iniciar sesión en tu cuenta de GitHub + Follow the prompts to complete the pairing process - Sigue las indicaciones para completar el proceso de emparejamiento + Re-enter the "sunnylink" panel to verify sponsorship status - Vuelve a ingresar al panel de "sunnylink" para verificar el estado de patrocinio + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - Si el estado de patrocinio no se actualizó, por favor contacta a un moderador en Discord en https://discord.gg/sunnypilot + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - Escanea el código QR para visitar la página de GitHub Sponsors de sunnyhaibin + Choose your sponsorship tier and confirm your support - Elige tu nivel de patrocinio y confirma tu apoyo + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - Únete a nuestra comunidad en Discord en https://discord.gg/sunnypilot y contacta a un moderador para confirmar tu estado de patrocinio + Pair your GitHub account - Empareja tu cuenta de GitHub + Early Access: Become a sunnypilot Sponsor - Acceso temprano: conviértete en patrocinador de sunnypilot + @@ -2262,10 +2120,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Disengage on Accelerator Pedal Desactivar con el Acelerador - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - Cuando esté activado, presionar el acelerador deshabilitará sunnypilot. - Enable Lane Departure Warnings Activar Avisos de Salida de Carril @@ -2278,10 +2132,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Always-On Driver Monitoring Monitoreo Permanente del Conductor - - Enable driver monitoring even when sunnypilot is not engaged. - Habilitar el monitoreo del conductor incluso cuando Openpilot no esté activado. - Record and Upload Driver Camera Grabar y Subir Cámara del Conductor @@ -2314,22 +2164,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Driving Personality Personalidad de conducción - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Se recomienda el modo estándar. En el modo agresivo, sunnypilot seguirá más cerca a los autos delante suyo y será más agresivo con el acelerador y el freno. En modo relajado, sunnypilot se mantendrá más alejado de los autos delante suyo. En automóviles compatibles, puede recorrer estas personalidades con el botón de distancia del volante. - - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - sunnypilot por defecto conduce en <b>modo chill</b>. El modo Experimental activa <b>funcionalidades en fase experimental</b>, que no están listas para el modo chill. Las funcionalidades del modo expeimental están listados abajo: - End-to-End Longitudinal Control Control Longitudinal de Punta a Punta - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - Dajar que el modelo de conducción controle la aceleración y el frenado. sunnypilot conducirá como piensa que lo haría una persona, incluiyendo parar en los semáforos en rojo y las señales de alto. Dado que el modelo decide la velocidad de conducción, la velocidad de crucero establecida solo actuará como el límite superior. Este recurso aún está en fase experimental; deberían esperarse errores. - New Driving Visualization Nueva Visualización de la conducción @@ -2343,12 +2181,38 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device El modo Experimental no está disponible actualmente para este auto, ya que el ACC default del auto está siendo usado para el control longitudinal. - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - Se puede probar una versión experimental del control longitudinal sunnypilot, junto con el modo Experimental, en ramas no liberadas. + openpilot longitudinal control may come in a future update. + El control longitudinal de openpilot podrá llegar en futuras actualizaciones. - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - Activar el control longitudinal (fase experimental) para permitir el modo Experimental. + Changing this setting will restart openpilot if the car is powered on. + Cambiar esta configuración reiniciará Openpilot si el automóvil está encendido. + + + Record and Upload Microphone Audio + Grabar y cargar audio de micrófono + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + Graba y almacena el audio del micrófono mientras conduces. El audio se incluirá en el video de la cámara del tablero en comma connect. + + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2359,27 +2223,31 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - Enable sunnypilot - Activar sunnypilot - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Record and Upload Microphone Audio + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. @@ -2394,41 +2262,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Cancelar - - Updater - - Update Required - Actualización Requerida - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - Es necesario la actualización del sistema operativo. Conecte su dispositivo al Wi-Fi para una actualización rápida. El tamaño de descarga es de aproximadamente 1GB. - - - Connect to Wi-Fi - Conectarse a la Wi-Fi - - - Install - Instalar - - - Back - Volver - - - Loading... - Cargando... - - - Reboot - Reiniciar - - - Update failed - Actualización fallida - - VisualsPanel @@ -2441,7 +2274,7 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Changing this setting will restart openpilot if the car is powered on. - + Cambiar esta configuración reiniciará Openpilot si el automóvil está encendido. Off @@ -2476,15 +2309,15 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device WiFiPromptWidget Open - - - - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - + Abrir Maximize your training data uploads to improve openpilot's driving models. - + Maximice sus cargas de datos de entrenamiento para mejorar los modelos de conducción de openpilot. + + + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> Modo Firehose <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 5b11081b0c..a86ac3ea64 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -7,10 +7,6 @@ Close Fermer - - Snooze Update - Reporter la mise à jour - Reboot and Update Redémarrer et mettre à jour @@ -167,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - Vous devez accepter les conditions générales pour utiliser sunnypilot. - Back Retour @@ -179,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 Refuser, désinstaller %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -190,6 +186,14 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode Mode manœuvre longitudinale + + openpilot Longitudinal Control (Alpha) + Contrôle longitudinal openpilot (Alpha) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + ATTENTION : le contrôle longitudinal openpilot est en alpha pour cette voiture et désactivera le freinage d'urgence automatique (AEB). + Enable ADB @@ -198,14 +202,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. @@ -305,10 +301,6 @@ This only toggles the visibility of the controls; it does not toggle the actual REVIEW REVOIR - - Review the rules, features, and limitations of sunnypilot - Revoir les règles, fonctionnalités et limitations d'sunnypilot - Are you sure you want to review the training guide? Êtes-vous sûr de vouloir revoir le guide de formation ? @@ -398,7 +390,7 @@ This only toggles the visibility of the controls; it does not toggle the actual - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. @@ -422,12 +414,20 @@ Steering lag calibration is complete. - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + Review the rules, features, and limitations of sunnypilot + + + + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -444,6 +444,10 @@ Steering lag calibration is complete. Language + + Reset Settings + + Are you sure you want to review the training guide? Êtes-vous sûr de vouloir revoir le guide de formation ? @@ -456,6 +460,19 @@ Steering lag calibration is complete. Select a language Choisir une langue + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot Redémarrer @@ -474,7 +491,7 @@ Steering lag calibration is complete. Confirm - Confirmer + Are you sure you want to enter Always Offroad mode? @@ -484,22 +501,6 @@ Steering lag calibration is complete. Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -513,16 +514,11 @@ Steering lag calibration is complete. - Wake-Up Behavior + Exit Always Offroad - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. + Always Offroad @@ -580,7 +576,7 @@ This is the time after which settings UI closes automatically if user is not int Confirm - Confirmer + EXIT ALWAYS OFFROAD MODE @@ -608,10 +604,6 @@ This is the time after which settings UI closes automatically if user is not int ACTIVE - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -619,6 +611,14 @@ This is the time after which settings UI closes automatically if user is not int + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + + + + Firehose Mode + + sunnypilot learns to drive by watching humans, like you, drive. @@ -626,11 +626,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - - - - Firehose Mode + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -739,6 +735,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -755,14 +759,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform supports limited MADS settings. - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - LongitudinalPanel @@ -794,11 +790,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -809,18 +809,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -837,6 +825,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -849,6 +841,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -895,66 +891,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECT SÉLECTIONNER - - downloaded - - - - ready - - - - from cache - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Réinitialiser la calibration - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - Continuer - - - on Metered - - - - Cancel - Annuler - Clear Model Cache @@ -996,6 +932,18 @@ The default software delay value is 0.2 %1 - %2 + + downloaded + + + + ready + + + + from cache + + download failed - %1 @@ -1004,12 +952,40 @@ The default software delay value is 0.2 pending - %1 + + Fetching models... + + + + Select a Model + + + + Default + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad. - Current: + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + Réinitialiser la calibration + + + Driving Model Selector @@ -1020,6 +996,22 @@ The default software delay value is 0.2 Clear Cache + + Warning: You are on a metered connection! + + + + Continue + Continuer + + + on Metered + + + + Cancel + Annuler + MultiOptionDialog @@ -1127,14 +1119,6 @@ The default software delay value is 0.2 Device temperature too high. System cooling down before starting. Current internal component temperature: %1 Température de l'appareil trop élevée. Le système doit refroidir avant de démarrer. Température actuelle de l'appareil : %1 - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - Connectez-vous immédiatement à internet pour vérifier les mises à jour. Si vous ne vous connectez pas à internet, sunnypilot ne s'engagera pas dans %1 - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - Connectez l'appareil à internet pour vérifier les mises à jour. sunnypilot ne démarrera pas automatiquement tant qu'il ne se connecte pas à internet pour vérifier les mises à jour. - Unable to download updates %1 @@ -1149,25 +1133,41 @@ The default software delay value is 0.2 An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. Une mise à jour du système d'exploitation de votre appareil est en cours de téléchargement en arrière-plan. Vous serez invité à effectuer la mise à jour lorsqu'elle sera prête à être installée. - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - L'appareil n'a pas réussi à s'enregistrer. Il ne se connectera pas aux serveurs de comma.ai, n'enverra rien et ne recevra aucune assistance de comma.ai. S'il s'agit d'un appareil officiel, visitez https://comma.ai/support. - NVMe drive not mounted. Le disque NVMe n'est pas monté. - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - Disque NVMe non supporté détecté. L'appareil peut consommer beaucoup plus d'énergie et surchauffer en raison du NVMe non supporté. + Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. + + + + Acknowledge Excessive Actuation + + + + Snooze Update + Reporter la mise à jour + + + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - sunnypilot n'a pas pu identifier votre voiture. Votre voiture n'est pas supportée ou ses ECUs ne sont pas reconnues. Veuillez soumettre un pull request pour ajouter les versions de firmware au véhicule approprié. Besoin d'aide ? Rejoignez discord.comma.ai. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilot a détecté un changement dans la position de montage de l'appareil. Assurez-vous que l'appareil est totalement inséré dans le support et que le support est fermement fixé au pare-brise. + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. @@ -1200,10 +1200,6 @@ The default software delay value is 0.2 OnroadAlerts - - sunnypilot Unavailable - sunnypilot indisponible - TAKE CONTROL IMMEDIATELY REPRENEZ LE CONTRÔLE IMMÉDIATEMENT @@ -1220,6 +1216,10 @@ The default software delay value is 0.2 System Unresponsive Système inopérant + + sunnypilot Unavailable + + OsmPanel @@ -1437,7 +1437,7 @@ Warning: You are on a metered connection! Confirm - Confirmer + Cancel @@ -1520,10 +1520,6 @@ Warning: You are on a metered connection! QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1549,46 +1545,9 @@ Warning: You are on a metered connection! now maintenant - - - Reset - Reset failed. Reboot to try again. - Réinitialisation échouée. Redémarrez pour réessayer. - - - Resetting device... -This may take up to a minute. - Réinitialisation de l'appareil... -Cela peut prendre jusqu'à une minute. - - - Are you sure you want to reset your device? - Êtes-vous sûr de vouloir réinitialiser votre appareil ? - - - System Reset - Réinitialisation du système - - - Cancel - Annuler - - - Reboot - Redémarrer - - - Confirm - Confirmer - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - Impossible de monter la partition data. La partition peut être corrompue. Appuyez sur confirmer pour effacer et réinitialiser votre appareil. - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - Réinitialisation système déclenchée. Appuyez sur confirmer pour effacer tout le contenu et les paramètres. Appuyez sur annuler pour reprendre le démarrage. + sunnypilot + @@ -1649,29 +1608,13 @@ Cela peut prendre jusqu'à une minute. Logiciel - Trips - - - - Vehicle - - - - Developer - Dév. - - - Firehose + Models Steering - - Models - - Cruise @@ -1684,111 +1627,22 @@ Cela peut prendre jusqu'à une minute. OSM - - - Setup - Something went wrong. Reboot the device. - Un problème est survenu. Redémarrez l'appareil. - - - Ensure the entered URL is valid, and the device’s internet connection is good. - Assurez-vous que l'URL saisie est valide et que la connexion internet de l'appareil est bonne. - - - No custom software found at this URL. - Aucun logiciel personnalisé trouvé à cette URL. - - - WARNING: Low Voltage - ATTENTION : Tension faible - - - Power your device in a car with a harness or proceed at your own risk. - Alimentez votre appareil dans une voiture avec un harness ou continuez à vos risques et périls. - - - Power off - Éteindre - - - Continue - Continuer - - - Getting Started - Commencer - - - Before we get on the road, let’s finish installation and cover some details. - Avant de prendre la route, terminons l'installation et passons en revue quelques détails. - - - Connect to Wi-Fi - Se connecter au Wi-Fi - - - Back - Retour - - - Enter URL - Entrer l'URL - - - for Custom Software - pour logiciel personnalisé - - - Continue without Wi-Fi - Continuer sans Wi-Fi - - - Waiting for internet - En attente d'internet - - - Downloading... - Téléchargement... - - - Download Failed - Échec du téléchargement - - - Reboot device - Redémarrer l'appareil - - - Start over - Recommencer - - - Select a language - Choisir une langue - - - Choose Software to Install - Choisir le logiciel à installer - - - sunnypilot - sunnypilot - - - Custom Software - Logiciel personnalisé - - - WARNING: Custom Software + Trips - Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. - -If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + Vehicle + + Firehose + + + + Developer + Dév. + SetupWidget @@ -1992,14 +1846,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enter search keywords, or leave blank to list all branches. - - No branches found for keywords: %1 - - - - Select a branch - Sélectionner une branche - Disable Updates @@ -2008,6 +1854,14 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 + + + + Select a branch + Sélectionner une branche + %1 updates requires a reboot.<br>Reboot now? @@ -2081,22 +1935,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - N/A - Sponsor Status @@ -2121,24 +1959,20 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + N/A + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -2193,6 +2027,26 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -2262,10 +2116,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Disengage on Accelerator Pedal Désengager avec la pédale d'accélérateur - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - Lorsqu'il est activé, appuyer sur la pédale d'accélérateur désengagera sunnypilot. - Enable Lane Departure Warnings Activer les avertissements de sortie de voie @@ -2306,14 +2156,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Driving Personality Personnalité de conduite - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - Par défaut, sunnypilot conduit en <b>mode détente</b>. Le mode expérimental permet d'activer des <b>fonctionnalités alpha</b> qui ne sont pas prêtes pour le mode détente. Les fonctionnalités expérimentales sont listées ci-dessous : - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - Laissez le modèle de conduite contrôler l'accélérateur et les freins. sunnypilot conduira comme il pense qu'un humain le ferait, y compris s'arrêter aux feux rouges et aux panneaux stop. Comme le modèle de conduite décide de la vitesse à adopter, la vitesse définie ne servira que de limite supérieure. Cette fonctionnalité est de qualité alpha ; des erreurs sont à prévoir. - New Driving Visualization Nouvelle visualisation de la conduite @@ -2323,21 +2165,13 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Le mode expérimental est actuellement indisponible pour cette voiture car le régulateur de vitesse adaptatif d'origine est utilisé pour le contrôle longitudinal. - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - Une version alpha du contrôle longitudinal sunnypilot peut être testée, avec le mode expérimental, sur des branches non publiées. + openpilot longitudinal control may come in a future update. + Le contrôle longitudinal openpilot pourrait être disponible dans une future mise à jour. End-to-End Longitudinal Control Contrôle longitudinal de bout en bout - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - Activer le contrôle longitudinal d'sunnypilot (en alpha) pour autoriser le mode expérimental. - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Le mode Standard est recommandé. En mode Agressif, sunnypilot suivra les véhicules de plus près et sera plus dynamique avec l'accélérateur et le frein. En mode Détendu, sunnypilot maintiendra une distance plus importante avec les véhicules qui précèdent. Sur les véhicules compatibles, vous pouvez alterner entre ces personnalités à l'aide du bouton de distance au volant. - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. La visualisation de la conduite passera sur la caméra grand angle dirigée vers la route à faible vitesse afin de mieux montrer certains virages. Le logo du mode expérimental s'affichera également dans le coin supérieur droit. @@ -2347,8 +2181,34 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Surveillance continue du conducteur - Enable driver monitoring even when sunnypilot is not engaged. - Activer la surveillance conducteur lorsque sunnypilot n'est pas actif. + Changing this setting will restart openpilot if the car is powered on. + + + + Record and Upload Microphone Audio + + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2359,27 +2219,31 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - Enable sunnypilot - Activer sunnypilot - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Record and Upload Microphone Audio + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. @@ -2394,41 +2258,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Annuler - - Updater - - Update Required - Mise à jour requise - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - Une mise à jour du système d'exploitation est requise. Connectez votre appareil au Wi-Fi pour une mise à jour plus rapide. La taille du téléchargement est d'environ 1 Go. - - - Connect to Wi-Fi - Se connecter au Wi-Fi - - - Install - Installer - - - Back - Retour - - - Loading... - Chargement... - - - Reboot - Redémarrer - - - Update failed - Échec de la mise à jour - - VisualsPanel @@ -2479,11 +2308,11 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. - Maximize your training data uploads to improve openpilot's driving models. + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index dae752b14c..0da704cd47 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -7,10 +7,6 @@ Close 閉じる - - Snooze Update - 更新の一時停止 - Reboot and Update 再起動してアップデート @@ -84,27 +80,27 @@ Prevent large data uploads when on a metered cellular connection - + モバイルデータ回線を使用しているときは大容量データをアップロードしません default - + 標準設定 metered - + 従量制 unmetered - + 定額制 Wi-Fi Network Metered - + 従量制のWi-Fiネットワーク Prevent large data uploads when on a metered Wi-Fi connection - + 通信制限のあるWi-Fi接続では大容量データをアップロードしません @@ -167,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - sunnypilotを使用するためには利用規約に同意する必要があります - Back 戻る @@ -179,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 同意しない(%1をアンインストール) + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -190,6 +186,14 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode アクセル制御マニューバー + + openpilot Longitudinal Control (Alpha) + openpilotアクセル制御(Alpha) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + この車ではopenpilotのアクセル制御はアルファ版であり、自動緊急ブレーキ(AEB)が無効化されます。 + Enable ADB ADBを有効にする @@ -198,14 +202,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB(Android Debug Bridge)により、USBまたはネットワーク経由でデバイスに接続できます。詳細は、https://docs.comma.ai/how-to/connect-to-comma を参照してください。 - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. @@ -301,10 +297,6 @@ This only toggles the visibility of the controls; it does not toggle the actual REVIEW 確認 - - Review the rules, features, and limitations of sunnypilot - sunnypilotのルール、機能、および制限を確認してください - Are you sure you want to review the training guide? トレーニングガイドを始めてもよろしいですか? @@ -395,39 +387,51 @@ This only toggles the visibility of the controls; it does not toggle the actual Disengage to Reset Calibration + キャリブレーションをリセットするには運転支援を解除して下さい。 + + + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + openpilot は継続的にキャリブレーションを行っており、リセットが必要になることはほとんどありません。キャリブレーションをリセットすると、車の電源が入っている場合はopenpilotが再起動します。 + + + + +Steering lag calibration is %1% complete. + + +ステアリング遅延のキャリブレーションが%1%完了。 + + + + +Steering lag calibration is complete. + + +ステアリング遅延のキャリブレーション完了。 + + + Steering torque response calibration is %1% complete. + ステアリングトルク応答のキャリブレーションが%1%完了。 + + + Steering torque response calibration is complete. + ステアリングトルク応答のキャリブレーション完了。 + + + Review the rules, features, and limitations of sunnypilot sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - -Steering lag calibration is %1% complete. - - - - - -Steering lag calibration is complete. - - - - Steering torque response calibration is %1% complete. - - - - Steering torque response calibration is complete. - - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - - DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -444,6 +448,10 @@ Steering lag calibration is complete. Language + + Reset Settings + + Are you sure you want to review the training guide? トレーニングガイドを始めてもよろしいですか? @@ -456,6 +464,19 @@ Steering lag calibration is complete. Select a language 言語を選択 + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot 再起動 @@ -474,7 +495,7 @@ Steering lag calibration is complete. Confirm - 確認 + Are you sure you want to enter Always Offroad mode? @@ -484,22 +505,6 @@ Steering lag calibration is complete. Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -513,16 +518,11 @@ Steering lag calibration is complete. - Wake-Up Behavior + Exit Always Offroad - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. + Always Offroad @@ -580,7 +580,7 @@ This is the time after which settings UI closes automatically if user is not int Confirm - 確認 + EXIT ALWAYS OFFROAD MODE @@ -602,22 +602,26 @@ This is the time after which settings UI closes automatically if user is not int FirehosePanel Firehose Mode: ACTIVE - + Firehoseモード: 作動中 ACTIVE - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - + 動作中 <b>%n segment(s)</b> of your driving is in the training dataset so far. - - + + あなたの運転の<b>%nセグメント</b>がこれまでのトレーニングデータに含まれています。 + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>動作停止</span>: 大容量のネットワークに接続してください + + + Firehose Mode + Firehoseモード + sunnypilot learns to drive by watching humans, like you, drive. @@ -625,11 +629,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>動作停止</span>: 大容量のネットワークに接続してください - - - Firehose Mode + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -737,6 +737,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -753,14 +761,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform supports limited MADS settings. - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - LongitudinalPanel @@ -792,11 +792,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -807,18 +811,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -835,6 +827,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -847,6 +843,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -893,66 +893,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECT 選択 - - downloaded - - - - ready - - - - from cache - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - キャリブレーションリセット - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - 続ける - - - on Metered - - - - Cancel - キャンセル - Clear Model Cache @@ -994,6 +934,18 @@ The default software delay value is 0.2 %1 - %2 + + downloaded + + + + ready + + + + from cache + + download failed - %1 @@ -1002,12 +954,40 @@ The default software delay value is 0.2 pending - %1 + + Fetching models... + + + + Select a Model + + + + Default + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad. - Current: + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + キャリブレーションリセット + + + Driving Model Selector @@ -1018,6 +998,22 @@ The default software delay value is 0.2 Clear Cache + + Warning: You are on a metered connection! + + + + Continue + 続ける + + + on Metered + + + + Cancel + キャンセル + MultiOptionDialog @@ -1121,14 +1117,6 @@ The default software delay value is 0.2 OffroadAlert - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - インターネットへ接続してアップデートを確認してください。未接続のままではsunnypilotを使用できなくなります。あと[%1] - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - インターネットに接続してアップデートを確認してください。接続するまでsunnypilotは使用できません。 - Unable to download updates %1 @@ -1143,29 +1131,45 @@ The default software delay value is 0.2 An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. オペレーティングシステムがバックグラウンドでダウンロードされています。インストールの準備が整うと更新を促されます。 - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - 登録に失敗しました。このデバイスはcomma.aiのサーバーに接続したりデータをアップロードしたりできません。またcomma.aiのサポートも受けられません。公式デバイスである場合は https://comma.ai/support に問い合わせて下さい。 - NVMe drive not mounted. SSDドライブ(NVMe)がマウントされていません。 - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - 非サポートのSSDドライブ(NVMe)が検出されました。このドライブを使用するとデバイスが多大な電力を消費し過熱する可能性があります。 + Device temperature too high. System cooling down before starting. Current internal component temperature: %1 + デバイスの温度が高すぎるためシステム起動前の冷却中です。現在のデバイス内部温度: %1 + + + Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. + 製品のcomma.aiへの登録に失敗しました。このデバイスはcomma.aiのサーバーに接続したりアップロードを行ったりすることはできず、comma.aiからのサポートも受けられません。もしcomma.ai/shopで購入した場合は、https://comma.ai/support にてサポートチケットをご提出ください。 + + + Acknowledge Excessive Actuation + 過剰な作動の検知 + + + Snooze Update + また後で更新する + + + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - sunnypilotが車両を識別できませんでした。車が未対応またはECUが認識されていない可能性があります。該当車両のファームウェアバージョンを追加するためにプルリクエストしてください。サポートが必要な場合は discord.comma.ai に参加することができます。 + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilotがデバイスの取り付け位置にずれを検出しました。デバイスの固定とマウントがフロントガラスにしっかりと取り付けられていることを確認してください。 - - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - デバイスの温度が高すぎるためシステム起動前の冷却中です。現在のデバイス内部温度: %1 + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. @@ -1198,10 +1202,6 @@ The default software delay value is 0.2 OnroadAlerts - - sunnypilot Unavailable - sunnypilotは使用できません - TAKE CONTROL IMMEDIATELY 直ちに車の運転に戻って下さい @@ -1218,6 +1218,10 @@ The default software delay value is 0.2 System Unresponsive システムが応答しません + + sunnypilot Unavailable + + OsmPanel @@ -1435,7 +1439,7 @@ Warning: You are on a metered connection! Confirm - 確認 + Cancel @@ -1518,10 +1522,6 @@ Warning: You are on a metered connection! QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1544,46 +1544,9 @@ Warning: You are on a metered connection! now たった今 - - - Reset - Reset failed. Reboot to try again. - 初期化に失敗しました。再起動後に再試行してください。 - - - Are you sure you want to reset your device? - 初期化してもよろしいですか? - - - System Reset - システムを初期化 - - - Cancel - キャンセル - - - Reboot - 再起動 - - - Confirm - 確認 - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - Dataパーティションをマウントできません。パーティションが破損している可能性があります。デバイスを消去してリセットしますので確認を押して下さい。 - - - Resetting device... -This may take up to a minute. - デバイスをリセットしています… -この処理には最大で1分ほどかかる場合があります。 - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - システムリセットの準備が整いました。すべてのデータと設定を消去するには「確認」を押してください。「キャンセル」を押すとブートを再開します。 + sunnypilot + @@ -1644,29 +1607,13 @@ This may take up to a minute. ソフト - Trips + Models - - Vehicle - - - - Developer - 開発 - - - Firehose - データ学習 - Steering - - Models - - Cruise @@ -1679,111 +1626,22 @@ This may take up to a minute. OSM - - - Setup - WARNING: Low Voltage - 警告:電圧低下 - - - Power your device in a car with a harness or proceed at your own risk. - ハーネスを使って車でデバイスに電源を供給するか、自己責任でこのまま継続して下さい。 - - - Power off - 電源を切る - - - Continue - 続ける - - - Getting Started - はじめに - - - Before we get on the road, let’s finish installation and cover some details. - 出発する前に、インストールを完了させて少し詳細を確認しましょう。 - - - Connect to Wi-Fi - Wi-Fiに接続 - - - Back - 戻る - - - Continue without Wi-Fi - Wi-Fiに接続せずに続行 - - - Waiting for internet - インターネット接続を待機中 - - - Enter URL - URLの入力 - - - for Custom Software - カスタムソフトウェア - - - Downloading... - ダウンロード中... - - - Download Failed - ダウンロードに失敗しました - - - Ensure the entered URL is valid, and the device’s internet connection is good. - 入力されたURLが正しいかどうか、インターネットに正常に接続できているかを確認してください。 - - - Reboot device - デバイスを再起動 - - - Start over - やり直す - - - No custom software found at this URL. - このURLはカスタムソフトウェアではありません。 - - - Something went wrong. Reboot the device. - 何かの問題が発生しました。デバイスを再起動してください。 - - - Select a language - 言語を選択 - - - Choose Software to Install - インストールするソフトウェアを選択してください。 - - - sunnypilot - sunnypilot - - - Custom Software - カスタムソフトウェア - - - WARNING: Custom Software + Trips - Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. - -If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + Vehicle + + Firehose + データ学習 + + + Developer + 開発 + SetupWidget @@ -1987,14 +1845,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enter search keywords, or leave blank to list all branches. - - No branches found for keywords: %1 - - - - Select a branch - ブランチを選択 - Disable Updates @@ -2003,6 +1853,14 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 + + + + Select a branch + ブランチを選択 + %1 updates requires a reboot.<br>Reboot now? @@ -2028,7 +1886,7 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username other than your own. A comma employee will NEVER ask you to add their GitHub username. - 警告: これは、GitHub の設定にあるすべての公開鍵への SSH アクセスを許可するものです。自分以外の GitHub のユーザー名を入力しないでください。commaのスタッフが GitHub のユーザー名を追加するようお願いすることはありません。 + 警告: これはGitHubの設定にあるすべての公開鍵への SSH アクセスを許可するものです。自分以外のGitHubユーザー名を入力しないでください。commaのスタッフがGitHubのユーザー名を追加するようお願いすることはありません。 ADD @@ -2036,7 +1894,7 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enter your GitHub username - GitHub のユーザー名を入力してください + GitHubのユーザー名を入力してください LOADING @@ -2076,22 +1934,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - 該当なし - Sponsor Status @@ -2116,24 +1958,20 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + 該当なし + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -2188,6 +2026,26 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -2251,7 +2109,7 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device TogglesPanel Enable Lane Departure Warnings - 車線逸脱警報機能の有効化 + 車線逸脱警報の有効化 Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). @@ -2277,22 +2135,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Disengage on Accelerator Pedal アクセルを踏むと運転サポートを中断 - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - この機能を有効化すると、sunnypilotを利用中にアクセルを踏むとsunnypilotによる運転サポートを中断します。 - Experimental Mode Experimentalモード - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - sunnypilotは標準ではゆっくりとくつろげる運転を提供します。このExperimental(実験)モードを有効にすると、以下のアグレッシブな開発中の機能を利用する事ができます。 - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - sunnypilotにアクセルとブレーキを任せます。sunnypilotは赤信号や一時停止サインでの停止を含み、人間と同じように考えて運転を行います。sunnypilotが運転速度を決定するため、あなたが設定する速度は上限速度になります。この機能は実験段階のため、sunnypilotの運転ミスに常に備えて注意してください。 - New Driving Visualization 新しい運転画面 @@ -2322,16 +2168,8 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device End-to-Endアクセル制御 - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - sunnypilotのアルファ版アクセル制御は、Experimentalモードと共に非リリースのブランチでテストすることができます。 - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - sunnypilotのアクセル制御機能(アルファ)を有効にして、Experimentalモードを許可してください。 - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - 標準モードが推奨されます。アグレッシブモードではsunnypilotは先行車に近づいて追従し、アクセルとブレーキがより強気になります。リラックスモードではsunnypilotは先行車から距離を取って走行します。サポートされている車両ではステアリングホイールの距離ボタンでこれらのモードを切り替えることができます。 + openpilot longitudinal control may come in a future update. + openpilotのアクセル制御は将来のアップデートで利用できる可能性があります。 The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. @@ -2342,8 +2180,34 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device 運転者の常時モニタリング - Enable driver monitoring even when sunnypilot is not engaged. - sunnypilotが作動していない場合でも運転者モニタリングを有効にする。 + Changing this setting will restart openpilot if the car is powered on. + この設定を変更すると車の電源が入っている場合はopenpilotが再起動します。 + + + Record and Upload Microphone Audio + マイク音声の録音とアップロード + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + 運転中にマイク音声を録音・保存します。音声は comma connect のドライブレコーダー映像に含まれます。 + + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2354,27 +2218,31 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - Enable sunnypilot - sunnypilot を有効化 - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Record and Upload Microphone Audio + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. @@ -2389,41 +2257,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device キャンセル - - Updater - - Update Required - アップデートが必要です - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - OSのアップデートが必要です。Wi-Fiに接続してアップデートする事をお勧めします。ダウンロードサイズは約1GBです。 - - - Connect to Wi-Fi - Wi-Fiに接続 - - - Install - インストール - - - Back - 戻る - - - Loading... - 読み込み中... - - - Reboot - 再起動 - - - Update failed - 更新失敗 - - VisualsPanel @@ -2436,7 +2269,7 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Changing this setting will restart openpilot if the car is powered on. - + この設定を変更すると車の電源が入っている場合はopenpilotが再起動します。 Off @@ -2474,12 +2307,12 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device 開く - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehoseモード <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. + openpilotの運転モデルを改善するために、大容量の学習データをアップロードして下さい。 - Maximize your training data uploads to improve openpilot's driving models. - + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehoseモード <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 86c8e10e26..e5a7c413ee 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -7,10 +7,6 @@ Close 닫기 - - Snooze Update - 업데이트 일시 중지 - Reboot and Update 업데이트 및 재부팅 @@ -60,7 +56,7 @@ Cellular Metered - 데이터 요금제 + 모바일 데이터 종량제 Hidden Network @@ -84,27 +80,27 @@ Prevent large data uploads when on a metered cellular connection - + 모바일 데이터 종량제 사용 시 대용량 데이터 업로드 방지 default - + 기본 metered - + 종량제 unmetered - + 무제한 Wi-Fi Network Metered - + 제한된 Wi-Fi 네트워크 Prevent large data uploads when on a metered Wi-Fi connection - + 제한된 Wi-Fi 사용 시 대용량 데이터 업로드 방지 @@ -167,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - sunnypilot을 사용하려면 이용약관에 동의해야 합니다. - Back 뒤로 @@ -179,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 거절, %1 제거 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -188,7 +184,15 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode - 롱컨 제어 모드 + 가감속 제어 조작 모드 + + + openpilot Longitudinal Control (Alpha) + 오픈파일럿 가감속 제어 (알파) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + 경고: 오픈파일럿 가감속 제어는 알파 기능으로 차량의 자동긴급제동(AEB)기능이 작동하지 않습니다. Enable ADB @@ -198,14 +202,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB (안드로이드 디버그 브릿지) USB 또는 네트워크를 통해 장치에 연결할 수 있습니다. 자세한 내용은 https://docs.comma.ai/how-to/connect-to-comma를 참조하세요. - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. @@ -301,10 +297,6 @@ This only toggles the visibility of the controls; it does not toggle the actual REVIEW 다시보기 - - Review the rules, features, and limitations of sunnypilot - sunnypilot의 규칙, 기능 및 제한 다시 확인 - Are you sure you want to review the training guide? 트레이닝 가이드를 다시 확인하시겠습니까? @@ -395,39 +387,51 @@ This only toggles the visibility of the controls; it does not toggle the actual Disengage to Reset Calibration + 캘리브레이션을 재설정하려면 해제하세요 + + + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + 오픈파일럿은 지속적으로 갤리브레이션되어 재설정이 거의 필요하지 않습니다. 차량과 연결된 경우 캘리브레이션 재설정이 오픈파일럿을 재시작합니다. + + + + +Steering lag calibration is %1% complete. + + +조향 지연 캘리브레이션이 %1% 진행되었습니다. + + + + +Steering lag calibration is complete. + + +조향 지연 캘리브레이션이 완료되었습니다. + + + Steering torque response calibration is %1% complete. + 조향 토크 응답 캘리브레이션이 %1% 진행되었습니다. + + + Steering torque response calibration is complete. + 조향 토크 응답 캘리브레이션이 완료되었습니다. + + + Review the rules, features, and limitations of sunnypilot sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - -Steering lag calibration is %1% complete. - - - - - -Steering lag calibration is complete. - - - - Steering torque response calibration is %1% complete. - - - - Steering torque response calibration is complete. - - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - - DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -444,6 +448,10 @@ Steering lag calibration is complete. Language + + Reset Settings + + Are you sure you want to review the training guide? 트레이닝 가이드를 다시 확인하시겠습니까? @@ -456,6 +464,19 @@ Steering lag calibration is complete. Select a language 언어를 선택하세요 + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot 재부팅 @@ -474,7 +495,7 @@ Steering lag calibration is complete. Confirm - 확인 + Are you sure you want to enter Always Offroad mode? @@ -484,22 +505,6 @@ Steering lag calibration is complete. Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -513,16 +518,11 @@ Steering lag calibration is complete. - Wake-Up Behavior + Exit Always Offroad - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. + Always Offroad @@ -580,7 +580,7 @@ This is the time after which settings UI closes automatically if user is not int Confirm - 확인 + EXIT ALWAYS OFFROAD MODE @@ -606,18 +606,22 @@ This is the time after which settings UI closes automatically if user is not int ACTIVE - 활성화 - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - 최대한의 효과를 얻으려면 매주 장치를 실내로 가져와 좋은 USB-C 케이블에 연결하고 Wi-Fi에 연결하세요.<br><br>파이어호스 모드는 핫스팟 또는 무제한 SIM 카드에 연결된 경우에는 운전 중에도 작동할 수 있습니다.<br><br><br><b>자주 묻는 질문</b><br><br><i>운전 방법이나 장소가 중요한가요?</i> 아니요, 평소처럼 운전하시면 됩니다.<br><br><i>파이어호스 모드에서 제 모든 구간을 가져오나요?.<br><br><i> 아니요, 저희는 여러분의 구간 중 일부를 선별적으로 가져옵니다.<br><br><i>좋은 USB-C 케이블은 무엇인가요?</i> 휴대폰이나 노트북 고속 충전기가 있으면 됩니다.<br><br><i>어떤 소프트웨어를 실행하는지가 중요한가요?</i> 예, 오직 공식 sunnypilot의 특정 포크만 트레이닝에 사용할 수 있습니다. + 활성 상태 <b>%n segment(s)</b> of your driving is in the training dataset so far. - - + + <b>%n 구간</b> 의 운전이 지금까지의 학습 데이터셋에 포함되어 있습니다. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>비활성 상태</span>: 무제한 네트워크에 연결 하세요 + + + Firehose Mode + 파이어호스 모드 + sunnypilot learns to drive by watching humans, like you, drive. @@ -625,11 +629,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - - - - Firehose Mode + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -737,6 +737,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -753,14 +761,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform supports limited MADS settings. - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - LongitudinalPanel @@ -792,11 +792,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -807,18 +811,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -835,6 +827,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -847,6 +843,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -893,66 +893,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECT 선택 - - downloaded - - - - ready - - - - from cache - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - 캘리브레이션 - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - 계속 - - - on Metered - - - - Cancel - 취소 - Clear Model Cache @@ -994,6 +934,18 @@ The default software delay value is 0.2 %1 - %2 + + downloaded + + + + ready + + + + from cache + + download failed - %1 @@ -1002,12 +954,40 @@ The default software delay value is 0.2 pending - %1 + + Fetching models... + + + + Select a Model + + + + Default + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad. - Current: + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + 캘리브레이션 + + + Driving Model Selector @@ -1018,6 +998,22 @@ The default software delay value is 0.2 Clear Cache + + Warning: You are on a metered connection! + + + + Continue + 계속 + + + on Metered + + + + Cancel + 취소 + MultiOptionDialog @@ -1121,14 +1117,6 @@ The default software delay value is 0.2 OffroadAlert - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - 즉시 인터넷에 연결하여 업데이트를 확인하세요. 인터넷에 연결되어 있지 않으면 %1 이후에는 sunnypilot이 활성화되지 않습니다. - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - 업데이트를 확인하려면 인터넷에 연결하세요. sunnypilot은 업데이트를 확인하기 위해 인터넷에 연결할 때까지 자동으로 시작되지 않습니다. - Unable to download updates %1 @@ -1143,29 +1131,45 @@ The default software delay value is 0.2 An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. 백그라운드에서 운영 체제에 대한 업데이트가 다운로드되고 있습니다. 설치가 준비되면 업데이트 메시지가 표시됩니다. - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - 장치를 등록하지 못했습니다. comma.ai 서버에 연결하거나 데이터를 업로드하지 않으며 comma.ai에서 지원을 받지 않습니다. 공식 장치인 경우 https://comma.ai/support 에 방문하여 문의하세요. - NVMe drive not mounted. NVMe 드라이브가 마운트되지 않았습니다. - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - 지원되지 않는 NVMe 드라이브가 감지되었습니다. 지원되지 않는 NVMe 드라이브는 많은 전력을 소비하고 장치를 과열시킬 수 있습니다. + Device temperature too high. System cooling down before starting. Current internal component temperature: %1 + 장치 온도가 너무 높습니다. 시작하기 전에 시스템을 냉각하고 있습니다. 현재 내부 구성 요소 온도: %1 + + + Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. + 장치를 comma.ai 백엔드에 등록하지 못했습니다. comma.ai 서버에 연결하거나 업로드하지 않으며 comma.ai로부터 지원을받지 않습니다. comma.ai shop에서 구매 한 장치 인 경우 https://comma.ai/support에서 티켓을 여십시오. + + + Acknowledge Excessive Actuation + 과도한 작동을 인정하십시오 + + + Snooze Update + 업데이트 일시 중지 + + + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - sunnypilot이 차량을 식별할 수 없습니다. 지원되지 않는 차량이거나 ECU가 인식되지 않습니다. 해당 차량에 맞는 펌웨어 버전을 추가하려면 PR을 제출하세요. 도움이 필요하시면 discord.comma.ai에 가입하세요. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilot 장치의 장착 위치가 변경되었습니다. 장치가 마운트에 완전히 장착되고 마운트가 앞유리에 단단히 고정되었는지 확인하세요. - - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - 장치의 온도가 너무 높습니다. 시작 전에 온도를 낮춰주세요. 현재 내부 구성 요소 온도: %1 + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. @@ -1198,10 +1202,6 @@ The default software delay value is 0.2 OnroadAlerts - - sunnypilot Unavailable - 오픈파일럿을 사용할수없습니다 - TAKE CONTROL IMMEDIATELY 핸들을 잡아주세요 @@ -1218,6 +1218,10 @@ The default software delay value is 0.2 System Unresponsive 시스템이 응답하지않습니다 + + sunnypilot Unavailable + + OsmPanel @@ -1435,7 +1439,7 @@ Warning: You are on a metered connection! Confirm - 확인 + Cancel @@ -1482,7 +1486,7 @@ Warning: You are on a metered connection! Become a comma prime member at connect.comma.ai - connect.comma.ai에 접속하여 comma prime 회원으로 등록하세요 + connect.comma.ai에서 comma prime 사용자로 등록하세요 PRIME FEATURES: @@ -1498,7 +1502,7 @@ Warning: You are on a metered connection! 1 year of drive storage - 1년간 드라이브 로그 저장 + 1년간 주행 로그 저장 Remote snapshots @@ -1518,10 +1522,6 @@ Warning: You are on a metered connection! QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1544,46 +1544,9 @@ Warning: You are on a metered connection! now now - - - Reset - Reset failed. Reboot to try again. - 초기화 실패. 재부팅 후 다시 시도하세요. - - - Are you sure you want to reset your device? - 장치를 초기화하시겠습니까? - - - System Reset - 장치 초기화 - - - Cancel - 취소 - - - Reboot - 재부팅 - - - Confirm - 확인 - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - 데이터 파티션을 마운트할 수 없습니다. 파티션이 손상되었을 수 있습니다. 모든 설정을 삭제하고 장치를 초기화하려면 확인을 누르세요. - - - Resetting device... -This may take up to a minute. - 장치를 초기화하는 중... -최대 1분이 소요될 수 있습니다. - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - 시스템 재설정이 시작되었습니다. 모든 콘텐츠와 설정을 지우려면 확인을 누르시고 부팅을 재개하려면 취소를 누르세요. + sunnypilot + @@ -1644,29 +1607,13 @@ This may take up to a minute. 소프트웨어 - Trips + Models - - Vehicle - - - - Developer - 개발자 - - - Firehose - 파이어호스 - Steering - - Models - - Cruise @@ -1679,111 +1626,22 @@ This may take up to a minute. OSM - - - Setup - WARNING: Low Voltage - 경고: 전압이 낮습니다 - - - Power your device in a car with a harness or proceed at your own risk. - 장치를 하네스를 통해 차량 전원에 연결하세요. USB 전원에서는 예상치 못한 문제가 생길 수 있습니다. - - - Power off - 전원 끄기 - - - Continue - 계속 - - - Getting Started - 시작하기 - - - Before we get on the road, let’s finish installation and cover some details. - 출발 전 설정을 완료하고 세부 사항을 살펴봅니다. - - - Connect to Wi-Fi - Wi-Fi 연결 - - - Back - 뒤로 - - - Continue without Wi-Fi - Wi-Fi 연결 없이 진행 - - - Waiting for internet - 인터넷 연결 대기 중 - - - Enter URL - URL 입력 - - - for Custom Software - 커스텀 소프트웨어 - - - Downloading... - 다운로드 중... - - - Download Failed - 다운로드 실패 - - - Ensure the entered URL is valid, and the device’s internet connection is good. - 입력된 URL이 유효하고 인터넷 연결이 원활한지 확인하세요. - - - Reboot device - 장치 재부팅 - - - Start over - 다시 시작 - - - Something went wrong. Reboot the device. - 문제가 발생했습니다. 장치를 재부팅하세요. - - - No custom software found at this URL. - 이 URL에서 커스텀 소프트웨어를 찾을 수 없습니다. - - - Select a language - 언어를 선택하세요 - - - Choose Software to Install - 설치할 소프트웨어 선택 - - - sunnypilot - sunnypilot - - - Custom Software - 커스텀 소프트웨어 - - - WARNING: Custom Software + Trips - Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. - -If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + Vehicle + + Firehose + 파이어호스 + + + Developer + 개발자 + SetupWidget @@ -1987,14 +1845,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enter search keywords, or leave blank to list all branches. - - No branches found for keywords: %1 - - - - Select a branch - 브랜치 선택 - Disable Updates @@ -2003,6 +1853,14 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 + + + + Select a branch + 브랜치 선택 + %1 updates requires a reboot.<br>Reboot now? @@ -2076,22 +1934,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - N/A - Sponsor Status @@ -2116,24 +1958,20 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + N/A + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -2188,6 +2026,26 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -2240,11 +2098,11 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Welcome to sunnypilot - 오픈 파일럿에 오신 것을 환영합니다. + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - 오픈파일럿을 사용하려면 이용약관에 동의해야 합니다. 최신 약관은 <span style='color: #465BEA;'>https://comma.ai/terms</span> 에서 최신 약관을 읽은 후 계속하세요. + @@ -2277,29 +2135,21 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Disengage on Accelerator Pedal 가속페달 조작 시 해제 - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - 활성화된 경우 가속 페달을 밟으면 sunnypilot이 해제됩니다. - Experimental Mode 실험 모드 - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - sunnypilot은 기본적으로 <b>안정 모드</b>로 주행합니다. 실험 모드는 안정화되지 않은 <b>알파 수준의 기능</b>을 활성화합니다. 실험 모드의 기능은 아래와 같습니다: - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - sunnypilot의 주행모델이 가감속을 제어합니다. sunnypilot은 신호등과 정지 표지판을 보고 멈추는 것을 포함하여 인간이 운전하는 것처럼 생각하고 주행합니다. 주행 모델이 주행할 속도를 결정하므로 설정된 속도는 최대 주행 속도로만 기능합니다. 이 기능은 알파 수준이므로 사용에 각별히 주의해야 합니다. - New Driving Visualization 새로운 주행 시각화 Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - 차량에 장착된 ACC로 가감속을 제어하기 때문에 현재 이 차량에서는 실험 모드를 사용할 수 없습니다. + 차량의 ACC가 가감속 제어에 사용되기 때문에, 이 차량에서는 실험 모드를 사용할 수 없습니다. + + + openpilot longitudinal control may come in a future update. + 오픈파일럿 가감속 제어는 향후 업데이트에서 지원될 수 있습니다. Aggressive @@ -2317,22 +2167,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Driving Personality 주행 모드 - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - sunnypilot 가감속 제어 알파 버전은 비 릴리즈 브랜치에서 실험 모드와 함께 테스트할 수 있습니다. - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - 실험 모드를 사용하려면 sunnypilot E2E 가감속 제어 (알파) 토글을 활성화하세요. - End-to-End Longitudinal Control E2E 가감속 제어 - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - 표준 모드를 권장합니다. 공격적 모드의 sunnypilot은 선두 차량을 더 가까이 따라가고 가감속제어를 사용하여 더욱 공격적으로 움직입니다. 편안한 모드의 sunnypilot은 선두 차량으로부터 더 멀리 떨어져 있습니다. 지원되는 차량에서는 스티어링 휠 거리 버튼을 사용하여 이러한 특성을 순환할 수 있습니다. - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. 운전 시각화는 일부 회전을 더 잘 보여주기 위해 저속에서 도로를 향한 광각 카메라로 전환됩니다. 우측 상단에 실험 모드 로고가 표시됩니다. @@ -2342,8 +2180,34 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device 상시 운전자 모니터링 - Enable driver monitoring even when sunnypilot is not engaged. - sunnypilot이 활성화되지 않은 경우에도 드라이버 모니터링을 활성화합니다. + Changing this setting will restart openpilot if the car is powered on. + 이 설정을 변경하면 차량이 재가동된후 오픈파일럿이 시작됩니다. + + + Record and Upload Microphone Audio + 마이크 오디오 녹음 및 업로드 + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + 운전 중에 마이크 오디오를 녹음하고 저장하십시오. 오디오는 comma connect의 대시캠 비디오에 포함됩니다. + + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2354,27 +2218,31 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - Enable sunnypilot - sunnypilot 사용 - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Record and Upload Microphone Audio + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. @@ -2389,41 +2257,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device 취소 - - Updater - - Update Required - 업데이트 필요 - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - OS 업데이트가 필요합니다. 장치를 Wi-Fi에 연결하면 가장 빠르게 업데이트할 수 있습니다. 다운로드 크기는 약 1GB입니다. - - - Connect to Wi-Fi - Wi-Fi 연결 - - - Install - 설치 - - - Back - 뒤로 - - - Loading... - 로딩 중... - - - Reboot - 재부팅 - - - Update failed - 업데이트 실패 - - VisualsPanel @@ -2436,7 +2269,7 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Changing this setting will restart openpilot if the car is powered on. - + 이 설정을 변경하면 차량이 재가동된후 오픈파일럿이 시작됩니다. Off @@ -2474,12 +2307,12 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device 열기 - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> 파이어호스 모드 <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. + 오픈파일럿의 주행 모델 개선을 위해 학습 데이터 업로드를 최대화하세요. - Maximize your training data uploads to improve openpilot's driving models. - + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> 파이어호스 모드 <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 503441c467..8dde941e81 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -7,10 +7,6 @@ Close Fechar - - Snooze Update - Adiar Atualização - Reboot and Update Reiniciar e Atualizar @@ -84,27 +80,27 @@ Prevent large data uploads when on a metered cellular connection - + Previna o envio de grandes volumes de dados em conexões de celular com franquia de limite de dados default - + padrão metered - + limitada unmetered - + ilimitada Wi-Fi Network Metered - + Rede Wi-Fi com Franquia Prevent large data uploads when on a metered Wi-Fi connection - + Previna o envio de grandes volumes de dados em conexões Wi-Fi com franquia de limite de dados @@ -167,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - Você precisa aceitar os Termos e Condições para utilizar sunnypilot. - Back Voltar @@ -179,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 Rejeitar, desintalar %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -190,6 +186,14 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode Modo Longitudinal Maneuver + + openpilot Longitudinal Control (Alpha) + Controle Longitudinal openpilot (Embrionário) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + AVISO: o controle longitudinal openpilot está em estado embrionário para este carro e desativará a Frenagem Automática de Emergência (AEB). + Enable ADB Habilitar ADB @@ -198,14 +202,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB (Android Debug Bridge) permite conectar ao seu dispositivo por meio do USB ou através da rede. Veja https://docs.comma.ai/how-to/connect-to-comma para maiores informações. - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. @@ -301,10 +297,6 @@ This only toggles the visibility of the controls; it does not toggle the actual REVIEW REVISAR - - Review the rules, features, and limitations of sunnypilot - Revisar regras, aprimoramentos e limitações do sunnypilot - Are you sure you want to review the training guide? Tem certeza que quer rever o treinamento? @@ -395,39 +387,51 @@ This only toggles the visibility of the controls; it does not toggle the actual Disengage to Reset Calibration + Desacione para Resetar a Calibração + + + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + O openpilot está em constante calibração, raramente sendo necessário redefini-lo. Redefinir a calibração reiniciará o openpilot se o carro estiver ligado. + + + + +Steering lag calibration is %1% complete. + + +A calibração do atraso da direção está %1% concluída. + + + + +Steering lag calibration is complete. + + +A calibração do atraso da direção foi concluída. + + + Steering torque response calibration is %1% complete. + A calibração da resposta de torque da direção está %1% concluída. + + + Steering torque response calibration is complete. + A calibração da resposta do torque da direção foi concluída. + + + Review the rules, features, and limitations of sunnypilot sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - -Steering lag calibration is %1% complete. - - - - - -Steering lag calibration is complete. - - - - Steering torque response calibration is %1% complete. - - - - Steering torque response calibration is complete. - - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - - DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -444,6 +448,10 @@ Steering lag calibration is complete. Language + + Reset Settings + + Are you sure you want to review the training guide? Tem certeza que quer rever o treinamento? @@ -456,6 +464,19 @@ Steering lag calibration is complete. Select a language Selecione o Idioma + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot Reiniciar @@ -474,7 +495,7 @@ Steering lag calibration is complete. Confirm - Confirmar + Are you sure you want to enter Always Offroad mode? @@ -484,22 +505,6 @@ Steering lag calibration is complete. Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -513,16 +518,11 @@ Steering lag calibration is complete. - Wake-Up Behavior + Exit Always Offroad - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. + Always Offroad @@ -580,7 +580,7 @@ This is the time after which settings UI closes automatically if user is not int Confirm - Confirmar + EXIT ALWAYS OFFROAD MODE @@ -608,10 +608,6 @@ This is the time after which settings UI closes automatically if user is not int ACTIVE ATIVO - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - Para maior eficácia, leve seu dispositivo para dentro de casa e conecte-o a um bom adaptador USB-C e Wi-Fi semanalmente.<br><br>O Modo Firehose também pode funcionar enquanto você dirige, se estiver conectado a um hotspot ou a um chip SIM com dados ilimitados.<br><br><br><b>Perguntas Frequentes</b><br><br><i>Importa como ou onde eu dirijo?</i> Não, basta dirigir normalmente.<br><br><i>Todos os meus segmentos são enviados no Modo Firehose?</i> Não, selecionamos apenas um subconjunto dos seus segmentos.<br><br><i>Qual é um bom adaptador USB-C?</i> Qualquer carregador rápido de telefone ou laptop deve ser suficiente.<br><br><i>Importa qual software eu uso?</i> Sim, apenas o sunnypilot oficial (e alguns forks específicos) podem ser usados para treinamento. - <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -619,6 +615,14 @@ This is the time after which settings UI closes automatically if user is not int <b>%n segmentos</b> da sua direção estão no conjunto de dados de treinamento até agora. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INATIVO</span>: conecte-se a uma rede sem limite <br> de dados + + + Firehose Mode + Modo Firehose + sunnypilot learns to drive by watching humans, like you, drive. @@ -626,11 +630,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INATIVO</span>: conecte-se a uma rede sem limite <br> de dados - - - Firehose Mode + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -739,6 +739,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -755,14 +763,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform supports limited MADS settings. - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - LongitudinalPanel @@ -794,11 +794,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -809,18 +813,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -837,6 +829,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -849,6 +845,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -895,66 +895,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECT SELECIONE - - downloaded - - - - ready - - - - from cache - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Reinicializar Calibragem - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - Continuar - - - on Metered - - - - Cancel - Cancelar - Clear Model Cache @@ -996,6 +936,18 @@ The default software delay value is 0.2 %1 - %2 + + downloaded + + + + ready + + + + from cache + + download failed - %1 @@ -1004,12 +956,40 @@ The default software delay value is 0.2 pending - %1 + + Fetching models... + + + + Select a Model + + + + Default + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad. - Current: + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + Reinicializar Calibragem + + + Driving Model Selector @@ -1020,6 +1000,22 @@ The default software delay value is 0.2 Clear Cache + + Warning: You are on a metered connection! + + + + Continue + Continuar + + + on Metered + + + + Cancel + Cancelar + MultiOptionDialog @@ -1123,14 +1119,6 @@ The default software delay value is 0.2 OffroadAlert - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - Conecte-se imediatamente à internet para verificar se há atualizações. Se você não se conectar à internet em %1 não será possível acionar o sunnypilot. - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - Conecte-se à internet para verificar se há atualizações. O sunnypilot não será iniciado automaticamente até que ele se conecte à internet para verificar se há atualizações. - Unable to download updates %1 @@ -1145,29 +1133,45 @@ The default software delay value is 0.2 An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. Uma atualização para o sistema operacional do seu dispositivo está sendo baixada em segundo plano. Você será solicitado a atualizar quando estiver pronto para instalar. - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - Falha ao registrar o dispositivo. Ele não se conectará ou fará upload para os servidores comma.ai e não receberá suporte da comma.ai. Se este for um dispositivo oficial, visite https://comma.ai/support. - NVMe drive not mounted. Unidade NVMe não montada. - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - Unidade NVMe não suportada detectada. O dispositivo pode consumir significativamente mais energia e superaquecimento devido ao NVMe não suportado. + Device temperature too high. System cooling down before starting. Current internal component temperature: %1 + Temperatura do dispositivo muito alta. O sistema está sendo resfriado antes de iniciar. A temperatura atual do componente interno é: %1 + + + Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. + O dispositivo não conseguiu se registrar no backend da comma.ai. Ele não se conecta nem faz upload para os servidores da comma.ai e não recebe suporte da comma.ai. Se este for um dispositivo adquirido em comma.ai/shop, abra um ticket em https://comma.ai/support. + + + Acknowledge Excessive Actuation + Reconhecer Atuação Excessiva + + + Snooze Update + Adiar Atualização + + + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - O sunnypilot não conseguiu identificar o seu carro. Seu carro não é suportado ou seus ECUs não são reconhecidos. Envie um pull request para adicionar as versões de firmware ao veículo adequado. Precisa de ajuda? Junte-se discord.comma.ai. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - O sunnypilot detectou uma mudança na posição de montagem do dispositivo. Verifique se o dispositivo está totalmente encaixado no suporte e se o suporte está firmemente preso ao para-brisa. - - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - Temperatura do dispositivo muito alta. O sistema está sendo resfriado antes de iniciar. A temperatura atual do componente interno é: %1 + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. @@ -1200,10 +1204,6 @@ The default software delay value is 0.2 OnroadAlerts - - sunnypilot Unavailable - sunnypilot Indisponível - TAKE CONTROL IMMEDIATELY ASSUMA IMEDIATAMENTE @@ -1220,6 +1220,10 @@ The default software delay value is 0.2 System Unresponsive Sistema sem Resposta + + sunnypilot Unavailable + + OsmPanel @@ -1437,7 +1441,7 @@ Warning: You are on a metered connection! Confirm - Confirmar + Cancel @@ -1520,10 +1524,6 @@ Warning: You are on a metered connection! QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1549,46 +1549,9 @@ Warning: You are on a metered connection! now agora - - - Reset - Reset failed. Reboot to try again. - Reset falhou. Reinicie para tentar novamente. - - - Are you sure you want to reset your device? - Tem certeza que quer resetar seu dispositivo? - - - System Reset - Resetar Sistema - - - Cancel - Cancelar - - - Reboot - Reiniciar - - - Confirm - Confirmar - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - Não é possível montar a partição de dados. Partição corrompida. Confirme para apagar e redefinir o dispositivo. - - - Resetting device... -This may take up to a minute. - Redefinindo o dispositivo -Isso pode levar até um minuto. - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - Reinicialização do sistema acionada. Pressione confirmar para apagar todo o conteúdo e configurações. Pressione cancel para retomar a inicialização. + sunnypilot + @@ -1649,29 +1612,13 @@ Isso pode levar até um minuto. Software - Trips + Models - - Vehicle - - - - Developer - Desenvdor - - - Firehose - Firehose - Steering - - Models - - Cruise @@ -1684,111 +1631,22 @@ Isso pode levar até um minuto. OSM - - - Setup - WARNING: Low Voltage - ALERTA: Baixa Voltagem - - - Power your device in a car with a harness or proceed at your own risk. - Ligue seu dispositivo em um carro com um chicote ou prossiga por sua conta e risco. - - - Power off - Desligar - - - Continue - Continuar - - - Getting Started - Começando - - - Before we get on the road, let’s finish installation and cover some details. - Antes de pegarmos a estrada, vamos terminar a instalação e cobrir alguns detalhes. - - - Connect to Wi-Fi - Conectar ao Wi-Fi - - - Back - Voltar - - - Continue without Wi-Fi - Continuar sem Wi-Fi - - - Waiting for internet - Esperando pela internet - - - Enter URL - Preencher URL - - - for Custom Software - para o Software Customizado - - - Downloading... - Baixando... - - - Download Failed - Download Falhou - - - Ensure the entered URL is valid, and the device’s internet connection is good. - Garanta que a URL inserida é valida, e uma boa conexão à internet. - - - Reboot device - Reiniciar Dispositivo - - - Start over - Inicializar - - - No custom software found at this URL. - Não há software personalizado nesta URL. - - - Something went wrong. Reboot the device. - Algo deu errado. Reinicie o dispositivo. - - - Select a language - Selecione o Idioma - - - Choose Software to Install - Escolha o Software a ser Instalado - - - sunnypilot - sunnypilot - - - Custom Software - Software Customizado - - - WARNING: Custom Software + Trips - Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. - -If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + Vehicle + + Firehose + Firehose + + + Developer + Desenvdor + SetupWidget @@ -1992,14 +1850,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enter search keywords, or leave blank to list all branches. - - No branches found for keywords: %1 - - - - Select a branch - Selecione uma branch - Disable Updates @@ -2008,6 +1858,14 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 + + + + Select a branch + Selecione uma branch + %1 updates requires a reboot.<br>Reboot now? @@ -2081,22 +1939,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - N/A - Sponsor Status @@ -2121,24 +1963,20 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + N/A + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -2193,6 +2031,26 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -2245,11 +2103,11 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Welcome to sunnypilot - Bem vindo ao sunnypilot + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - Você deve aceitar os Termos e Condições para usar o sunnypilot. Leia os termos mais recentes em <span style='color: #465BEA;'>https://comma.ai/terms</span> antes de continuar. + @@ -2282,22 +2140,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Disengage on Accelerator Pedal Desacionar com Pedal do Acelerador - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - Quando ativado, pressionar o pedal do acelerador desacionará o sunnypilot. - Experimental Mode Modo Experimental - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - sunnypilot por padrão funciona em <b>modo chill</b>. modo Experimental ativa <b>recursos de nível-embrionário</b> que não estão prontos para o modo chill. Recursos experimentais estão listados abaixo: - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - Deixe o modelo de IA controlar o acelerador e os freios. O sunnypilot irá dirigir como pensa que um humano faria, incluindo parar em sinais vermelhos e sinais de parada. Uma vez que o modelo de condução decide a velocidade a conduzir, a velocidade definida apenas funcionará como um limite superior. Este é um recurso de qualidade embrionária; erros devem ser esperados. - New Driving Visualization Nova Visualização de Condução @@ -2306,6 +2152,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. O modo Experimental está atualmente indisponível para este carro já que o ACC original do carro é usado para controle longitudinal. + + openpilot longitudinal control may come in a future update. + O controle longitudinal openpilot poderá vir em uma atualização futura. + Aggressive Disputa @@ -2322,22 +2172,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Driving Personality Temperamento de Direção - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - Uma versão embrionária do controle longitudinal sunnypilot pode ser testada em conjunto com o modo Experimental, em branches que não sejam de produção. - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - Habilite o controle longitudinal (embrionário) sunnypilot para permitir o modo Experimental. - End-to-End Longitudinal Control Controle Longitudinal de Ponta a Ponta - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Neutro é o recomendado. No modo disputa o sunnypilot seguirá o carro da frente mais de perto e será mais agressivo com a aceleração e frenagem. No modo calmo o sunnypilot se manterá mais longe do carro da frente. Em carros compatíveis, você pode alternar esses temperamentos com o botão de distância do volante. - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. A visualização de condução fará a transição para a câmera grande angular voltada para a estrada em baixas velocidades para mostrar melhor algumas curvas. O logotipo do modo Experimental também será mostrado no canto superior direito. @@ -2347,8 +2185,34 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Monitoramento do Motorista Sempre Ativo - Enable driver monitoring even when sunnypilot is not engaged. - Habilite o monitoramento do motorista mesmo quando o sunnypilot não estiver acionado. + Changing this setting will restart openpilot if the car is powered on. + Alterar esta configuração fará com que o openpilot reinicie se o carro estiver ligado. + + + Record and Upload Microphone Audio + Gravar e Fazer Upload do Áudio do Microfone + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + Grave e armazene o áudio do microfone enquanto estiver dirigindo. O áudio será incluído ao vídeo dashcam no comma connect. + + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2359,27 +2223,31 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - Enable sunnypilot - Ativar sunnypilot - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Record and Upload Microphone Audio + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. @@ -2394,41 +2262,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Cancelar - - Updater - - Update Required - Atualização Necessária - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - Uma atualização do sistema operacional é necessária. Conecte seu dispositivo ao Wi-Fi para a experiência de atualização mais rápida. O tamanho do download é de aproximadamente 1GB. - - - Connect to Wi-Fi - Conecte-se ao Wi-Fi - - - Install - Instalar - - - Back - Voltar - - - Loading... - Carregando... - - - Reboot - Reiniciar - - - Update failed - Falha na atualização - - VisualsPanel @@ -2441,7 +2274,7 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Changing this setting will restart openpilot if the car is powered on. - + Alterar esta configuração fará com que o openpilot reinicie se o carro estiver ligado. Off @@ -2479,12 +2312,12 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Abrir - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> Modo Firehose <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. + Maximize seus envios de dados de treinamento para melhorar os modelos de direção do openpilot. - Maximize your training data uploads to improve openpilot's driving models. - + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> Modo Firehose <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index a3a65e9e03..1292ef37fd 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -7,10 +7,6 @@ Close ปิด - - Snooze Update - เลื่อนการอัปเดต - Reboot and Update รีบูตและอัปเดต @@ -167,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - คุณต้องยอมรับเงื่อนไขและข้อตกลง เพื่อใช้งาน sunnypilot - Back ย้อนกลับ @@ -179,32 +171,36 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 ปฏิเสธ และถอนการติดตั้ง %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel Joystick Debug Mode - + โหมดดีบักจอยสติ๊ก Longitudinal Maneuver Mode - - - - Enable ADB - - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - + โหมดการควบคุมการเร่ง/เบรค openpilot Longitudinal Control (Alpha) - + ระบบควบคุมการเร่ง/เบรคโดย openpilot (Alpha) WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - + คำเตือน: การควบคุมการเร่ง/เบรคโดย openpilot สำหรับรถคันนี้ยังอยู่ในสถานะ alpha และระบบเบรคฉุกเฉินอัตโนมัติ (AEB) จะถูกปิด + + + Enable ADB + เปิด ADB + + + ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. + ADB (Android Debug Bridge) อนุญาตให้เชื่อมต่ออุปกรณ์ของคุณผ่าน USB หรือผ่านเครือข่าย ดูข้อมูลเพิ่มเติมที่ https://docs.comma.ai/how-to/connect-to-comma On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. @@ -301,10 +297,6 @@ This only toggles the visibility of the controls; it does not toggle the actual REVIEW ทบทวน - - Review the rules, features, and limitations of sunnypilot - ตรวจสอบกฎ คุณสมบัติ และข้อจำกัดของ sunnypilot - Are you sure you want to review the training guide? คุณแน่ใจหรือไม่ว่าต้องการทบทวนคู่มือการใช้งาน? @@ -398,7 +390,7 @@ This only toggles the visibility of the controls; it does not toggle the actual - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. @@ -422,12 +414,20 @@ Steering lag calibration is complete. - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + Review the rules, features, and limitations of sunnypilot + + + + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -444,6 +444,10 @@ Steering lag calibration is complete. Language + + Reset Settings + + Are you sure you want to review the training guide? คุณแน่ใจหรือไม่ว่าต้องการทบทวนคู่มือการใช้งาน? @@ -456,6 +460,19 @@ Steering lag calibration is complete. Select a language เลือกภาษา + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot รีบูต @@ -474,7 +491,7 @@ Steering lag calibration is complete. Confirm - ยืนยัน + Are you sure you want to enter Always Offroad mode? @@ -484,22 +501,6 @@ Steering lag calibration is complete. Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -513,16 +514,11 @@ Steering lag calibration is complete. - Wake-Up Behavior + Exit Always Offroad - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. + Always Offroad @@ -580,7 +576,7 @@ This is the time after which settings UI closes automatically if user is not int Confirm - ยืนยัน + EXIT ALWAYS OFFROAD MODE @@ -602,22 +598,26 @@ This is the time after which settings UI closes automatically if user is not int FirehosePanel Firehose Mode: ACTIVE - + โหมดสายยางดับเพลิง: เปิดใช้งาน ACTIVE - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - + เปิดใช้งาน <b>%n segment(s)</b> of your driving is in the training dataset so far. - - + + มีการขับขี่ของคุณ <b>%n เซกเมนต์</b> อยู่ในชุดข้อมูลการฝึกฝนแล้วในขณะนี้ + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>ไม่เปิดใช้งาน</span>: เชื่อมต่อกับเครือข่ายที่ไม่จำกัดข้อมูล + + + Firehose Mode + + sunnypilot learns to drive by watching humans, like you, drive. @@ -625,11 +625,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - - - - Firehose Mode + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -737,6 +733,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -753,14 +757,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform supports limited MADS settings. - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - LongitudinalPanel @@ -792,11 +788,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -807,18 +807,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -835,6 +823,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -847,6 +839,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -893,66 +889,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECT เลือก - - downloaded - - - - ready - - - - from cache - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - รีเซ็ตการคาลิเบรท - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - ดำเนินการต่อ - - - on Metered - - - - Cancel - ยกเลิก - Clear Model Cache @@ -994,6 +930,18 @@ The default software delay value is 0.2 %1 - %2 + + downloaded + + + + ready + + + + from cache + + download failed - %1 @@ -1002,12 +950,40 @@ The default software delay value is 0.2 pending - %1 + + Fetching models... + + + + Select a Model + + + + Default + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad. - Current: + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + รีเซ็ตการคาลิเบรท + + + Driving Model Selector @@ -1018,6 +994,22 @@ The default software delay value is 0.2 Clear Cache + + Warning: You are on a metered connection! + + + + Continue + ดำเนินการต่อ + + + on Metered + + + + Cancel + ยกเลิก + MultiOptionDialog @@ -1125,14 +1117,6 @@ The default software delay value is 0.2 Device temperature too high. System cooling down before starting. Current internal component temperature: %1 อุณหภูมิของอุปกรณ์สูงเกินไป ระบบกำลังทำความเย็นก่อนเริ่ม อุณหภูมิของชิ้นส่วนภายในปัจจุบัน: %1 - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - กรุณาเชื่อมต่ออินเตอร์เน็ตเพื่อตรวจสอบอัปเดทเดี๋ยวนี้ ถ้าคุณไม่เชื่อมต่ออินเตอร์เน็ต sunnypilot จะไม่ทำงานในอีก %1 - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - กรุณาเชื่อมต่ออินเตอร์เน็ตเพื่อตรวจสอบอัปเดท sunnypilot จะไม่เริ่มทำงานอัตโนมัติจนกว่าจะได้เชื่อมต่อกับอินเตอร์เน็ตเพื่อตรวจสอบอัปเดท - Unable to download updates %1 @@ -1147,25 +1131,41 @@ The default software delay value is 0.2 An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. กำลังดาวน์โหลดอัปเดทสำหรับระบบปฏิบัติการอยู่เบื้องหลัง คุณจะได้รับการแจ้งเตือนเมื่อระบบพร้อมสำหรับการติดตั้ง - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - ไม่สามารถลงทะเบียนอุปกรณ์ได้ อุปกรณ์จะไม่สามารถเชื่อมต่อหรืออัปโหลดไปยังเซิร์ฟเวอร์ของ comma.ai ได้และจะไม่ได้รับการสนับสนุนจาก comma.ai ถ้านี่คืออุปกรณ์อย่างเป็นทางการ กรุณาติดต่อ https://comma.ai/support - NVMe drive not mounted. ไม่ได้ติดตั้งไดร์ฟ NVMe - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - ตรวจพบไดร์ฟ NVMe ที่ไม่รองรับ อุปกรณ์อาจใช้พลังงานมากขึ้นและร้อนเกินไปเนื่องจากไดร์ฟ NVMe ที่ไม่รองรับ + Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. + + + + Acknowledge Excessive Actuation + + + + Snooze Update + เลื่อนการอัปเดต + + + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - sunnypilot ไม่สามารถระบุรถยนต์ของคุณได้ ระบบอาจไม่รองรับรถยนต์ของคุณหรือไม่รู้จัก ECU กรุณาส่ง pull request เพื่อเพิ่มรุ่นของเฟิร์มแวร์ให้กับรถยนต์ที่เหมาะสม หากต้องการความช่วยเหลือให้เข้าร่วม discord.comma.ai + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilot ตรวจพบการเปลี่ยนแปลงของตำแหน่งที่ติดตั้ง กรุณาตรวจสอบว่าได้เลื่อนอุปกรณ์เข้ากับจุดติดตั้งจนสุดแล้ว และจุดติดตั้งได้ยึดติดกับกระจกหน้าอย่างแน่นหนา + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. @@ -1198,10 +1198,6 @@ The default software delay value is 0.2 OnroadAlerts - - sunnypilot Unavailable - sunnypilot ไม่สามารถใช้งานได้ - TAKE CONTROL IMMEDIATELY เข้าควบคุมรถเดี๋ยวนี้ @@ -1212,10 +1208,14 @@ The default software delay value is 0.2 Waiting to start - + รอเริ่มทำงาน System Unresponsive + ระบบไม่ตอบสนอง + + + sunnypilot Unavailable @@ -1370,7 +1370,7 @@ Warning: You are on a metered connection! Please connect to Wi-Fi to complete initial pairing - + กรุณาเชื่อมต่อ Wi-Fi เพื่อทำการจับคู่ครั้งแรกให้เสร็จสิ้น @@ -1435,7 +1435,7 @@ Warning: You are on a metered connection! Confirm - ยืนยัน + Cancel @@ -1502,7 +1502,7 @@ Warning: You are on a metered connection! Remote snapshots - + ภาพถ่ายระยะไกล @@ -1518,10 +1518,6 @@ Warning: You are on a metered connection! QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1544,46 +1540,9 @@ Warning: You are on a metered connection! now ตอนนี้ - - - Reset - Reset failed. Reboot to try again. - การรีเซ็ตล้มเหลว รีบูตเพื่อลองอีกครั้ง - - - Are you sure you want to reset your device? - คุณแน่ใจหรือไม่ว่าต้องการรีเซ็ตอุปกรณ์? - - - System Reset - รีเซ็ตระบบ - - - Cancel - ยกเลิก - - - Reboot - รีบูต - - - Confirm - ยืนยัน - - - Resetting device... -This may take up to a minute. - กำลังรีเซ็ตอุปกรณ์... -อาจใช้เวลาถึงหนึ่งนาที - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - ไม่สามารถเมานต์พาร์ติชั่นข้อมูลได้ พาร์ติชั่นอาจเสียหาย กดยืนยันเพื่อลบและรีเซ็ตอุปกรณ์ของคุณ - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - ระบบถูกรีเซ็ต กดยืนยันเพื่อลบข้อมูลและการตั้งค่าทั้งหมด กดยกเลิกเพื่อบูตต่อ + sunnypilot + @@ -1610,11 +1569,11 @@ This may take up to a minute. Developer - + นักพัฒนา Firehose - + สายยางดับเพลิง @@ -1644,29 +1603,13 @@ This may take up to a minute. ซอฟต์แวร์ - Trips - - - - Vehicle - - - - Developer - - - - Firehose + Models Steering - - Models - - Cruise @@ -1679,111 +1622,22 @@ This may take up to a minute. OSM - - - Setup - WARNING: Low Voltage - คำเตือน: แรงดันแบตเตอรี่ต่ำ - - - Power your device in a car with a harness or proceed at your own risk. - โปรดต่ออุปกรณ์ของคุณเข้ากับสายควบคุมในรถยนต์ หรือดำเนินการด้วยความเสี่ยงของคุณเอง - - - Power off - ปิดเครื่อง - - - Continue - ดำเนินการต่อ - - - Getting Started - เริ่มกันเลย - - - Before we get on the road, let’s finish installation and cover some details. - ก่อนออกเดินทาง เรามาทำการติดตั้งซอฟต์แวร์ และตรวจสอบการตั้งค่า - - - Connect to Wi-Fi - เชื่อมต่อ Wi-Fi - - - Back - ย้อนกลับ - - - Continue without Wi-Fi - ดำเนินการต่อโดยไม่ใช้ Wi-Fi - - - Waiting for internet - กำลังรอสัญญาณอินเตอร์เน็ต - - - Enter URL - ป้อน URL - - - for Custom Software - สำหรับซอฟต์แวร์ที่กำหนดเอง - - - Downloading... - กำลังดาวน์โหลด... - - - Download Failed - ดาวน์โหลดล้มเหลว - - - Ensure the entered URL is valid, and the device’s internet connection is good. - ตรวจสอบให้แน่ใจว่า URL ที่ป้อนนั้นถูกต้อง และอุปกรณ์เชื่อมต่ออินเทอร์เน็ตอยู่ - - - Reboot device - รีบูตอุปกรณ์ - - - Start over - เริ่มต้นใหม่ - - - Something went wrong. Reboot the device. - มีบางอย่างผิดพลาด รีบูตอุปกรณ์ - - - No custom software found at this URL. - ไม่พบซอฟต์แวร์ที่กำหนดเองที่ URL นี้ - - - Select a language - เลือกภาษา - - - Choose Software to Install - เลือกซอฟต์แวร์ที่จะติดตั้ง - - - sunnypilot - sunnypilot - - - Custom Software - ซอฟต์แวร์ที่กำหนดเอง - - - WARNING: Custom Software + Trips - Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. - -If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + Vehicle + + Firehose + สายยางดับเพลิง + + + Developer + นักพัฒนา + SetupWidget @@ -1987,14 +1841,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enter search keywords, or leave blank to list all branches. - - No branches found for keywords: %1 - - - - Select a branch - เลือก Branch - Disable Updates @@ -2003,6 +1849,14 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 + + + + Select a branch + เลือก Branch + %1 updates requires a reboot.<br>Reboot now? @@ -2076,22 +1930,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - ไม่มี - Sponsor Status @@ -2116,24 +1954,20 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + ไม่มี + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -2188,6 +2022,26 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -2277,22 +2131,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Disengage on Accelerator Pedal ยกเลิกระบบช่วยขับเมื่อเหยียบคันเร่ง - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - เมื่อเปิดใช้งาน การกดแป้นคันเร่งจะเป็นการยกเลิกระบบช่วยขับโดย sunnypilot - Experimental Mode โหมดทดลอง - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - โดยปกติ sunnypilot จะขับใน<b>โหมดชิล</b> เปิดโหมดทดลองเพื่อใช้<b>ความสามารถในขั้นพัฒนา</b> ซึ่งยังไม่พร้อมสำหรับโหมดชิล ความสามารถในขั้นพัฒนามีดังนี้: - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - ให้ sunnypilot ควบคุมการเร่ง/เบรค โดย sunnypilot จะขับอย่างที่มนุษย์คิด รวมถึงการหยุดที่ไฟแดง และป้ายหยุดรถ เนื่องจาก sunnypilot จะกำหนดความเร็วในการขับด้วยตัวเอง การตั้งความเร็วจะเป็นเพียงการกำหนดความเร็วสูงสูดเท่านั้น ความสามารถนี้ยังอยู่ในขั้นพัฒนา อาจเกิดข้อผิดพลาดขึ้นได้ - New Driving Visualization การแสดงภาพการขับขี่แบบใหม่ @@ -2301,6 +2143,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. ขณะนี้โหมดทดลองไม่สามารถใช้งานได้ในรถคันนี้ เนื่องจากเปิดใช้ระบบควบคุมการเร่ง/เบรคของรถที่ติดตั้งจากโรงงานอยู่ + + openpilot longitudinal control may come in a future update. + ระบบควบคุมการเร่ง/เบรคโดย openpilot อาจมาในการอัปเดตในอนาคต + Aggressive ดุดัน @@ -2317,32 +2163,46 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Driving Personality บุคลิกการขับขี่ - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - ระบบควบคุมการเร่ง/เบรคโดย sunnypilot เวอร์ชัน alpha สามารถทดสอบได้พร้อมกับโหมดการทดลอง บน branch ที่กำลังพัฒนา - End-to-End Longitudinal Control ควบคุมเร่ง/เบรคแบบ End-to-End - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - เปิดระบบควบคุมการเร่ง/เบรคโดย sunnypilot (alpha) เพื่อเปิดใช้งานโหมดทดลอง - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - แนะนำให้ใช้แบบมาตรฐาน ในโหมดดุดัน sunnypilot จะตามรถคันหน้าใกล้ขึ้นและเร่งและเบรคแบบดุดันมากขึ้น ในโหมดผ่อนคลาย sunnypilot จะอยู่ห่างจากรถคันหน้ามากขึ้น ในรถรุ่นที่รองรับคุณสามารถเปลี่ยนบุคลิกไปแบบต่าง ๆ โดยใช้ปุ่มปรับระยะห่างบนพวงมาลัย - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. การแสดงภาพการขับขี่จะเปลี่ยนไปใช้กล้องมุมกว้างที่หันหน้าไปทางถนนเมื่ออยู่ในความเร็วต่ำ เพื่อแสดงภาพการเลี้ยวที่ดีขึ้น โลโก้โหมดการทดลองจะแสดงที่มุมบนขวาด้วย Always-On Driver Monitoring + การเฝ้าระวังผู้ขับขี่ตลอดเวลา + + + Changing this setting will restart openpilot if the car is powered on. - Enable driver monitoring even when sunnypilot is not engaged. + Record and Upload Microphone Audio + + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. @@ -2354,27 +2214,31 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - Enable sunnypilot - เปิดใช้งาน sunnypilot - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Record and Upload Microphone Audio + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. @@ -2389,41 +2253,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device ยกเลิก - - Updater - - Update Required - จำเป็นต้องอัปเดต - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - จำเป็นต้องมีการอัปเดตระบบปฏิบัติการ เชื่อมต่ออุปกรณ์ของคุณกับ Wi-Fi เพื่อประสบการณ์การอัปเดตที่เร็วที่สุด ขนาดดาวน์โหลดประมาณ 1GB - - - Connect to Wi-Fi - เชื่อมต่อกับ Wi-Fi - - - Install - ติดตั้ง - - - Back - ย้อนกลับ - - - Loading... - กำลังโหลด... - - - Reboot - รีบูต - - - Update failed - การอัปเดตล้มเหลว - - VisualsPanel @@ -2471,15 +2300,15 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device WiFiPromptWidget Open - - - - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - + เปิด Maximize your training data uploads to improve openpilot's driving models. - + อัปโหลดข้อมูลการฝึกฝนให้ได้มากที่สุด เพื่อพัฒนาโมเดลการขับขี่ของ openpilot + + + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> โหมดสายยางดับเพลิง <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 3677ba0329..6b5a03bc70 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -7,10 +7,6 @@ Close Kapat - - Snooze Update - Güncellemeyi sessize al - Reboot and Update Güncelle ve Yeniden başlat @@ -167,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - Openpilotu kullanmak için Kullanıcı Koşullarını kabul etmelisiniz. - Back Geri @@ -179,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 Reddet, Kurulumu kaldır. %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -190,14 +186,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode - - Enable ADB - - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - - openpilot Longitudinal Control (Alpha) @@ -206,6 +194,14 @@ Please use caution when using this feature. Only use the blinker when traffic an WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + Enable ADB + + + + ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. @@ -301,10 +297,6 @@ This only toggles the visibility of the controls; it does not toggle the actual REVIEW GÖZDEN GEÇİR - - Review the rules, features, and limitations of sunnypilot - sunnypilot sisteminin kurallarını ve sınırlamalarını gözden geçirin. - Are you sure you want to review the training guide? Eğitim kılavuzunu incelemek istediğinizden emin misiniz? @@ -398,7 +390,7 @@ This only toggles the visibility of the controls; it does not toggle the actual - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. @@ -422,12 +414,20 @@ Steering lag calibration is complete. - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + Review the rules, features, and limitations of sunnypilot + + + + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -444,6 +444,10 @@ Steering lag calibration is complete. Language + + Reset Settings + + Are you sure you want to review the training guide? Eğitim kılavuzunu incelemek istediğinizden emin misiniz? @@ -456,6 +460,19 @@ Steering lag calibration is complete. Select a language Dil seçin + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot Yeniden başlat @@ -474,7 +491,7 @@ Steering lag calibration is complete. Confirm - Onayla + Are you sure you want to enter Always Offroad mode? @@ -484,22 +501,6 @@ Steering lag calibration is complete. Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -513,16 +514,11 @@ Steering lag calibration is complete. - Wake-Up Behavior + Exit Always Offroad - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. + Always Offroad @@ -580,7 +576,7 @@ This is the time after which settings UI closes automatically if user is not int Confirm - Onayla + EXIT ALWAYS OFFROAD MODE @@ -608,16 +604,20 @@ This is the time after which settings UI closes automatically if user is not int ACTIVE - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - <b>%n segment(s)</b> of your driving is in the training dataset so far. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + + + + Firehose Mode + + sunnypilot learns to drive by watching humans, like you, drive. @@ -625,11 +625,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - - - - Firehose Mode + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -737,6 +733,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -753,14 +757,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform supports limited MADS settings. - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - LongitudinalPanel @@ -792,11 +788,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -807,18 +807,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -835,6 +823,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -847,6 +839,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -893,66 +889,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECT - - downloaded - - - - ready - - - - from cache - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Kalibrasyonu sıfırla - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - Devam et - - - on Metered - - - - Cancel - - Clear Model Cache @@ -994,6 +930,18 @@ The default software delay value is 0.2 %1 - %2 + + downloaded + + + + ready + + + + from cache + + download failed - %1 @@ -1002,12 +950,40 @@ The default software delay value is 0.2 pending - %1 + + Fetching models... + + + + Select a Model + + + + Default + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad. - Current: + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + Kalibrasyonu sıfırla + + + Driving Model Selector @@ -1018,6 +994,22 @@ The default software delay value is 0.2 Clear Cache + + Warning: You are on a metered connection! + + + + Continue + Devam et + + + on Metered + + + + Cancel + + MultiOptionDialog @@ -1125,14 +1117,6 @@ The default software delay value is 0.2 Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - - Unable to download updates %1 @@ -1146,16 +1130,32 @@ The default software delay value is 0.2 An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - - NVMe drive not mounted. - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. + Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. + + + + Acknowledge Excessive Actuation + + + + Snooze Update + Güncellemeyi sessize al + + + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. @@ -1197,10 +1197,6 @@ The default software delay value is 0.2 OnroadAlerts - - sunnypilot Unavailable - - TAKE CONTROL IMMEDIATELY @@ -1217,6 +1213,10 @@ The default software delay value is 0.2 System Unresponsive + + sunnypilot Unavailable + + OsmPanel @@ -1434,7 +1434,7 @@ Warning: You are on a metered connection! Confirm - Onayla + Cancel @@ -1517,10 +1517,6 @@ Warning: You are on a metered connection! QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1543,44 +1539,8 @@ Warning: You are on a metered connection! now - - - Reset - Reset failed. Reboot to try again. - Sıfırlama başarız oldu. Cihazı yeniden başlatın ve tekrar deneyin. - - - Are you sure you want to reset your device? - Cihazı sıfırlamak istediğinizden eminmisiniz ? - - - System Reset - Sistemi sıfırla - - - Cancel - İptal - - - Reboot - Yeniden başlat - - - Confirm - Onayla - - - Resetting device... -This may take up to a minute. - - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. + sunnypilot @@ -1642,29 +1602,13 @@ This may take up to a minute. Yazılım - Trips - - - - Vehicle - - - - Developer - - - - Firehose + Models Steering - - Models - - Cruise @@ -1677,109 +1621,20 @@ This may take up to a minute. OSM - - - Setup - WARNING: Low Voltage - UYARI: Düşük voltaj - - - Power your device in a car with a harness or proceed at your own risk. - Cihazınızı emniyet kemeri olan bir arabada çalıştırın veya riski kabul ederek devam edin. - - - Power off - Sistemi kapat - - - Continue - Devam et - - - Getting Started - Başlarken - - - Before we get on the road, let’s finish installation and cover some details. - Yola çıkmadan önce kurulumu bitirin ve bazı detayları gözden geçirin.. - - - Connect to Wi-Fi - Wi-Fi ile bağlan - - - Back - Geri - - - Continue without Wi-Fi - Wi-Fi bağlantısı olmadan devam edin - - - Waiting for internet - İnternet bağlantısı bekleniyor. - - - Enter URL - URL girin - - - for Custom Software - özel yazılım için - - - Downloading... - İndiriliyor... - - - Download Failed - İndirme başarısız. - - - Ensure the entered URL is valid, and the device’s internet connection is good. - Girilen URL nin geçerli olduğundan ve cihazın internet bağlantısının olduğunu kontrol edin - - - Reboot device - Cihazı yeniden başlat - - - Start over - Zacznij od początku - - - Something went wrong. Reboot the device. + Trips - No custom software found at this URL. + Vehicle - Select a language - Dil seçin - - - Choose Software to Install + Firehose - sunnypilot - sunnypilot - - - Custom Software - - - - WARNING: Custom Software - - - - Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. - -If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + Developer @@ -1985,14 +1840,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enter search keywords, or leave blank to list all branches. - - No branches found for keywords: %1 - - - - Select a branch - - Disable Updates @@ -2001,6 +1848,14 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 + + + + Select a branch + + %1 updates requires a reboot.<br>Reboot now? @@ -2074,22 +1929,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - N/A - Sponsor Status @@ -2114,24 +1953,20 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + N/A + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -2186,6 +2021,26 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -2271,10 +2126,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Upload data from the driver facing camera and help improve the driver monitoring algorithm. Sürücüye bakan kamera verisini yükleyin ve Cihazın algoritmasını geliştirmemize yardımcı olun. - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - Aktifleştirilirse eğer gaz pedalına basınca sunnypilot devre dışı kalır. - Experimental Mode @@ -2299,18 +2150,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Driving Personality - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - - End-to-End Longitudinal Control - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - - New Driving Visualization @@ -2320,15 +2163,7 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + openpilot longitudinal control may come in a future update. @@ -2340,7 +2175,33 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - Enable driver monitoring even when sunnypilot is not engaged. + Changing this setting will restart openpilot if the car is powered on. + + + + Record and Upload Microphone Audio + + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. @@ -2352,27 +2213,31 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - Enable sunnypilot - sunnypilot'u aktifleştir - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Record and Upload Microphone Audio + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. @@ -2387,41 +2252,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - - Updater - - Update Required - Güncelleme yapılması gerekli - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - İşletim sistemi güncellemesi gerekmektedir. Lütfen Cihazı daha hızlı günceleyebilmesi için bir sonraki başlatılışında çekilir. Wi-Fi ağına bağlayın. Dosyanın boyutu yaklaşık 1GB dır. - - - Connect to Wi-Fi - Wi-Fi ağına bağlan - - - Install - Yükle - - - Back - Geri - - - Loading... - Yükleniyor... - - - Reboot - Yeniden başlat - - - Update failed - Güncelleme başarız oldu - - VisualsPanel @@ -2472,11 +2302,11 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. - Maximize your training data uploads to improve openpilot's driving models. + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 987d42a7f7..ec2cde0f60 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -7,10 +7,6 @@ Close 关闭 - - Snooze Update - 暂停更新 - Reboot and Update 重启并更新 @@ -40,7 +36,7 @@ IP Address - IP地址 + IP 地址 Enable Roaming @@ -48,11 +44,11 @@ APN Setting - APN设置 + APN 设置 Enter APN - 输入APN + 输入 APN leave blank for automatic configuration @@ -84,27 +80,27 @@ Prevent large data uploads when on a metered cellular connection - + 在按流量计费的移动网络上,防止上传大数据 default - + 默认 metered - + 按流量计费 unmetered - + 不按流量计费 Wi-Fi Network Metered - + 按流量计费的 WLAN 网络 Prevent large data uploads when on a metered Wi-Fi connection - + 在按流量计费的 WLAN 网络上,防止上传大数据 @@ -167,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - 您必须接受条款和条件以使用sunnypilot。 - Back 返回 @@ -179,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 拒绝并卸载%1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -190,6 +186,14 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode 纵向操控测试模式 + + openpilot Longitudinal Control (Alpha) + openpilot纵向控制(Alpha 版) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + 警告:此车辆的 openpilot 纵向控制功能目前处于Alpha版本,使用此功能将会停用自动紧急制动(AEB)功能。 + Enable ADB 启用 ADB @@ -198,14 +202,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB(Android调试桥接)允许通过USB或网络连接到您的设备。更多信息请参见 [https://docs.comma.ai/how-to/connect-to-comma](https://docs.comma.ai/how-to/connect-to-comma)。 - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. @@ -301,10 +297,6 @@ This only toggles the visibility of the controls; it does not toggle the actual REVIEW 查看 - - Review the rules, features, and limitations of sunnypilot - 查看 sunnypilot 的使用规则,以及其功能和限制 - Are you sure you want to review the training guide? 您确定要查看新手指南吗? @@ -363,7 +355,7 @@ This only toggles the visibility of the controls; it does not toggle the actual Disengage to Reboot - 取消sunnypilot以重新启动 + 取消openpilot以重新启动 Are you sure you want to power off? @@ -371,7 +363,7 @@ This only toggles the visibility of the controls; it does not toggle the actual Disengage to Power Off - 取消sunnypilot以关机 + 取消openpilot以关机 Reset @@ -395,39 +387,51 @@ This only toggles the visibility of the controls; it does not toggle the actual Disengage to Reset Calibration + 解除以重置校准 + + + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + openpilot 会持续进行校准,因此很少需要重置。如果车辆电源已开启,重置校准会重新启动 openpilot。 + + + + +Steering lag calibration is %1% complete. + + +转向延迟校准已完成 %1%。 + + + + +Steering lag calibration is complete. + + +转向延迟校准已完成。 + + + Steering torque response calibration is %1% complete. + 转向扭矩响应校准已完成 %1%。 + + + Steering torque response calibration is complete. + 转向扭矩响应校准已完成。 + + + Review the rules, features, and limitations of sunnypilot sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - -Steering lag calibration is %1% complete. - - - - - -Steering lag calibration is complete. - - - - Steering torque response calibration is %1% complete. - - - - Steering torque response calibration is complete. - - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - - DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -444,6 +448,10 @@ Steering lag calibration is complete. Language + + Reset Settings + + Are you sure you want to review the training guide? 您确定要查看新手指南吗? @@ -456,6 +464,19 @@ Steering lag calibration is complete. Select a language 选择语言 + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot 重启 @@ -474,7 +495,7 @@ Steering lag calibration is complete. Confirm - 确认 + Are you sure you want to enter Always Offroad mode? @@ -484,22 +505,6 @@ Steering lag calibration is complete. Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -513,16 +518,11 @@ Steering lag calibration is complete. - Wake-Up Behavior + Exit Always Offroad - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. + Always Offroad @@ -580,7 +580,7 @@ This is the time after which settings UI closes automatically if user is not int Confirm - 确认 + EXIT ALWAYS OFFROAD MODE @@ -602,22 +602,26 @@ This is the time after which settings UI closes automatically if user is not int FirehosePanel Firehose Mode: ACTIVE - 训练数据上传模式:激活中 + Firehose 模式:激活中 ACTIVE 激活中 - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - 为了达到最佳效果,请每周将您的设备带回室内,并连接到优质的 USB-C 充电器和 Wi-Fi。<br><br>Firehose 模式在行驶时也能运行,但需连接到移动热点或使用不限流量的 SIM 卡。<br><br><br><b>常见问题</b><br><br><i>我开车的方式或地点有影响吗?</i>不会,请像平常一样驾驶即可。<br><br><i>Firehose 模式会上传所有的驾驶片段吗?</i>不会,我们会选择性地上传部分片段。<br><br><i>什么是好的 USB-C 充电器?</i>任何快速手机或笔记本电脑充电器都应该适用。<br><br><i>我使用的软件版本有影响吗?</i>有的,只有官方 sunnypilot(以及特定的分支)可以用于训练。 - <b>%n segment(s)</b> of your driving is in the training dataset so far. <b>目前已有 %n 段</b> 您的驾驶数据被纳入训练数据集。 + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>闲置</span>:请连接到不限流量的网络 + + + Firehose Mode + Firehose 模式 + sunnypilot learns to drive by watching humans, like you, drive. @@ -625,11 +629,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>闲置</span>:请连接到不限流量的网络 - - - Firehose Mode + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -737,6 +737,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -753,14 +761,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform supports limited MADS settings. - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - LongitudinalPanel @@ -792,11 +792,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -807,18 +811,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -835,6 +827,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -847,6 +843,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -893,66 +893,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECT 选择 - - downloaded - - - - ready - - - - from cache - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - 重置设备校准 - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - 继续 - - - on Metered - - - - Cancel - 取消 - Clear Model Cache @@ -994,6 +934,18 @@ The default software delay value is 0.2 %1 - %2 + + downloaded + + + + ready + + + + from cache + + download failed - %1 @@ -1002,12 +954,40 @@ The default software delay value is 0.2 pending - %1 + + Fetching models... + + + + Select a Model + + + + Default + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad. - Current: + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + 重置设备校准 + + + Driving Model Selector @@ -1018,6 +998,22 @@ The default software delay value is 0.2 Clear Cache + + Warning: You are on a metered connection! + + + + Continue + 继续 + + + on Metered + + + + Cancel + 取消 + MultiOptionDialog @@ -1121,14 +1117,6 @@ The default software delay value is 0.2 OffroadAlert - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - 请立即连接网络检查更新。如果不连接网络,sunnypilot 将在 %1 后便无法使用 - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - 请连接至互联网以检查更新。在连接至互联网并完成更新检查之前,sunnypilot 将不会自动启动。 - Unable to download updates %1 @@ -1143,29 +1131,45 @@ The default software delay value is 0.2 An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. 一个针对您设备的操作系统更新正在后台下载中。当更新准备好安装时,您将收到提示进行更新。 - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - 设备注册失败。它将无法连接或上传至 comma.ai 服务器,并且无法获得 comma.ai 的支持。如果这是一个官方设备,请访问 https://comma.ai/support。 - NVMe drive not mounted. NVMe固态硬盘未被挂载。 - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - 检测到不支持的 NVMe 固态硬盘。您的设备因为使用了不支持的 NVMe 固态硬盘可能会消耗更多电力并更易过热。 + Device temperature too high. System cooling down before starting. Current internal component temperature: %1 + 设备温度过高。系统正在冷却中,等冷却完毕后才会启动。目前内部组件温度:%1 + + + Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. + 设备未能注册到 comma.ai 后端。该设备将无法连接或上传数据到 comma.ai 服务器,也无法获得 comma.ai 的支持。如果该设备是在 comma.ai/shop 购买的,请访问 https://comma.ai/support 提交工单。 + + + Acknowledge Excessive Actuation + + + + Snooze Update + 暂停更新 + + + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - sunnypilot 无法识别您的车辆。您的车辆可能未被支持,或是其电控单元 (ECU) 未被识别。请提交一个 Pull Request 为您的车辆添加正确的固件版本。需要帮助吗?请加入 discord.comma.ai。 + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilot 检测到设备的安装位置发生变化。请确保设备完全安装在支架上,并确保支架牢固地固定在挡风玻璃上。 - - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - 设备温度过高。系统正在冷却中,等冷却完毕后才会启动。目前内部组件温度:%1 + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. @@ -1198,10 +1202,6 @@ The default software delay value is 0.2 OnroadAlerts - - sunnypilot Unavailable - 无法使用 sunnypilot - TAKE CONTROL IMMEDIATELY 立即接管 @@ -1218,6 +1218,10 @@ The default software delay value is 0.2 System Unresponsive 系统无响应 + + sunnypilot Unavailable + + OsmPanel @@ -1435,7 +1439,7 @@ Warning: You are on a metered connection! Confirm - 确认 + Cancel @@ -1518,10 +1522,6 @@ Warning: You are on a metered connection! QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1544,46 +1544,9 @@ Warning: You are on a metered connection! now 现在 - - - Reset - Reset failed. Reboot to try again. - 重置失败。 重新启动以重试。 - - - Are you sure you want to reset your device? - 您确定要重置您的设备吗? - - - System Reset - 恢复出厂设置 - - - Cancel - 取消 - - - Reboot - 重启 - - - Confirm - 确认 - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - 无法挂载数据分区。分区可能已经损坏。请确认是否要删除并重新设置。 - - - Resetting device... -This may take up to a minute. - 设备重置中… -这可能需要一分钟的时间。 - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - 系统重置已触发。按下“确认”以清除所有内容和设置,按下“取消”以继续启动。 + sunnypilot + @@ -1614,7 +1577,7 @@ This may take up to a minute. Firehose - 训练上传 + Firehose @@ -1644,29 +1607,13 @@ This may take up to a minute. 软件 - Trips + Models - - Vehicle - - - - Developer - 开发人员 - - - Firehose - 训练上传 - Steering - - Models - - Cruise @@ -1679,111 +1626,22 @@ This may take up to a minute. OSM - - - Setup - WARNING: Low Voltage - 警告:低电压 - - - Power your device in a car with a harness or proceed at your own risk. - 请使用car harness线束为您的设备供电,或自行承担风险。 - - - Power off - 关机 - - - Continue - 继续 - - - Getting Started - 开始设置 - - - Before we get on the road, let’s finish installation and cover some details. - 开始旅程之前,让我们完成安装并介绍一些细节。 - - - Connect to Wi-Fi - 连接到WiFi - - - Back - 返回 - - - Continue without Wi-Fi - 不连接WiFi并继续 - - - Waiting for internet - 等待网络连接 - - - Enter URL - 输入网址 - - - for Custom Software - 以下载自定义软件 - - - Downloading... - 正在下载…… - - - Download Failed - 下载失败 - - - Ensure the entered URL is valid, and the device’s internet connection is good. - 请确保互联网连接良好且输入的URL有效。 - - - Reboot device - 重启设备 - - - Start over - 重来 - - - No custom software found at this URL. - 在此网址找不到自定义软件。 - - - Something went wrong. Reboot the device. - 发生了一些错误。请重新启动您的设备。 - - - Select a language - 选择语言 - - - Choose Software to Install - 选择要安装的软件 - - - sunnypilot - sunnypilot - - - Custom Software - 定制软件 - - - WARNING: Custom Software + Trips - Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. - -If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + Vehicle + + Firehose + Firehose + + + Developer + 开发人员 + SetupWidget @@ -1987,14 +1845,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enter search keywords, or leave blank to list all branches. - - No branches found for keywords: %1 - - - - Select a branch - 选择分支 - Disable Updates @@ -2003,6 +1853,14 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 + + + + Select a branch + 选择分支 + %1 updates requires a reboot.<br>Reboot now? @@ -2076,22 +1934,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - N/A - Sponsor Status @@ -2116,24 +1958,20 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + N/A + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -2188,6 +2026,26 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -2240,11 +2098,11 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Welcome to sunnypilot - 欢迎使用 sunnypilot + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - 您必须接受《条款与条件》才能使用 sunnypilot。在继续之前,请先阅读最新条款:<span style='color: #465BEA;'>https://comma.ai/terms</span>。 + @@ -2277,22 +2135,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Disengage on Accelerator Pedal 踩油门时取消控制 - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - 启用后,踩下油门踏板将取消sunnypilot。 - Experimental Mode 测试模式 - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - sunnypilot 默认 <b>轻松模式</b>驾驶车辆。试验模式启用一些轻松模式之外的 <b>试验性功能</b>。试验性功能包括: - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - 允许驾驶模型控制加速和制动,sunnypilot将模仿人类驾驶车辆,包括在红灯和停车让行标识前停车。鉴于驾驶模型确定行驶车速,所设定的车速仅作为上限。此功能尚处于早期测试状态,有可能会出现操作错误。 - New Driving Visualization 新驾驶视角 @@ -2301,6 +2147,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. 由于此车辆使用自带的ACC纵向控制,当前无法使用试验模式。 + + openpilot longitudinal control may come in a future update. + openpilot纵向控制可能会在未来的更新中提供。 + Aggressive 积极 @@ -2317,22 +2167,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Driving Personality 驾驶风格 - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - 在正式(release)版本以外的分支上,可以测试 sunnypilot 纵向控制的 Alpha 版本以及实验模式。 - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - 启用 sunnypilot 纵向控制(alpha)开关以允许实验模式。 - End-to-End Longitudinal Control 端到端纵向控制 - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - 推荐使用标准模式。在积极模式下,sunnypilot 会更靠近前方车辆,并在油门和刹车方面更加激进。在放松模式下,sunnypilot 会与前方车辆保持更远距离。在支持的车型上,你可以使用方向盘上的距离按钮来循环切换这些驾驶风格。 - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. 在低速时,驾驶可视化将转换为道路朝向的广角摄像头,以更好地展示某些转弯。测试模式标志也将显示在右上角。 @@ -2342,8 +2180,34 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device 驾驶员监控常开 - Enable driver monitoring even when sunnypilot is not engaged. - 即使在sunnypilot未激活时也启用驾驶员监控。 + Changing this setting will restart openpilot if the car is powered on. + 如果车辆已通电,更改此设置将会重新启动 openpilot。 + + + Record and Upload Microphone Audio + 录制并上传麦克风音频 + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + 在驾驶时录制并存储麦克风音频。该音频将会包含在 comma connect 的行车记录仪视频中。 + + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2354,27 +2218,31 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - Enable sunnypilot - 启用sunnypilot - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Record and Upload Microphone Audio + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. @@ -2389,41 +2257,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device 取消 - - Updater - - Update Required - 需要更新 - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - 操作系统需要更新。请将您的设备连接到WiFi以获取更快的更新体验。下载大小约为1GB。 - - - Connect to Wi-Fi - 连接到WiFi - - - Install - 安装 - - - Back - 返回 - - - Loading... - 正在加载…… - - - Reboot - 重启 - - - Update failed - 更新失败 - - VisualsPanel @@ -2436,7 +2269,7 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Changing this setting will restart openpilot if the car is powered on. - + 如果车辆已通电,更改此设置将会重新启动 openpilot。 Off @@ -2474,12 +2307,12 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device 开启 - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> 训练数据上传模式 <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. + 最大化您的训练数据上传,以改善 openpilot 的驾驶模型。 - Maximize your training data uploads to improve openpilot's driving models. - + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose 模式 <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index c9692be62b..669d9b2053 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -7,10 +7,6 @@ Close 關閉 - - Snooze Update - 暫停更新 - Reboot and Update 重啟並更新 @@ -60,7 +56,7 @@ Cellular Metered - 行動網路 + 計費的行動網路 Hidden Network @@ -84,27 +80,27 @@ Prevent large data uploads when on a metered cellular connection - + 在使用計費行動網路時,防止上傳大量數據 default - + 預設 metered - + 計費 unmetered - + 非計費 Wi-Fi Network Metered - + 計費 Wi-Fi 網路 Prevent large data uploads when on a metered Wi-Fi connection - + 在使用計費 Wi-Fi 網路時,防止上傳大量數據 @@ -167,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - 您必須先接受條款和條件才能使用 sunnypilot。 - Back 回上頁 @@ -179,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 拒絕並解除安裝 %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -190,6 +186,14 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode 縱向操控測試模式 + + openpilot Longitudinal Control (Alpha) + openpilot 縱向控制 (Alpha 版) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + 警告:此車輛的 openpilot 縱向控制功能目前處於 Alpha 版本,使用此功能將會停用自動緊急煞車(AEB)功能。 + Enable ADB 啟用 ADB @@ -198,14 +202,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB(Android 調試橋接)允許通過 USB 或網絡連接到您的設備。更多信息請參見 [https://docs.comma.ai/how-to/connect-to-comma](https://docs.comma.ai/how-to/connect-to-comma)。 - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. @@ -301,10 +297,6 @@ This only toggles the visibility of the controls; it does not toggle the actual REVIEW 觀看 - - Review the rules, features, and limitations of sunnypilot - 觀看 sunnypilot 的使用規則、功能和限制 - Are you sure you want to review the training guide? 您確定要觀看使用教學嗎? @@ -395,39 +387,51 @@ This only toggles the visibility of the controls; it does not toggle the actual Disengage to Reset Calibration + 解除以重設校準 + + + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + openpilot 會持續進行校準,因此很少需要重設。若車輛電源開啟,重設校準將會重新啟動 openpilot。 + + + + +Steering lag calibration is %1% complete. + + +轉向延遲校準已完成 %1%。 + + + + +Steering lag calibration is complete. + + +轉向延遲校準已完成。 + + + Steering torque response calibration is %1% complete. + 轉向扭矩反應校準已完成 %1%。 + + + Steering torque response calibration is complete. + 轉向扭矩反應校準已完成。 + + + Review the rules, features, and limitations of sunnypilot sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - -Steering lag calibration is %1% complete. - - - - - -Steering lag calibration is complete. - - - - Steering torque response calibration is %1% complete. - - - - Steering torque response calibration is complete. - - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - - DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -444,6 +448,10 @@ Steering lag calibration is complete. Language + + Reset Settings + + Are you sure you want to review the training guide? 您確定要觀看使用教學嗎? @@ -456,6 +464,19 @@ Steering lag calibration is complete. Select a language 選擇語言 + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot 重新啟動 @@ -474,7 +495,7 @@ Steering lag calibration is complete. Confirm - 確認 + Are you sure you want to enter Always Offroad mode? @@ -484,22 +505,6 @@ Steering lag calibration is complete. Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -513,16 +518,11 @@ Steering lag calibration is complete. - Wake-Up Behavior + Exit Always Offroad - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. + Always Offroad @@ -580,7 +580,7 @@ This is the time after which settings UI closes automatically if user is not int Confirm - 確認 + EXIT ALWAYS OFFROAD MODE @@ -602,22 +602,26 @@ This is the time after which settings UI closes automatically if user is not int FirehosePanel Firehose Mode: ACTIVE - 訓練資料上傳模式:啟用中 + Firehose 模式:啟用中 ACTIVE 啟用中 - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - 為了達到最佳效果,請每週將您的裝置帶回室內,並連接至優質的 USB-C 充電器與 Wi-Fi。<br><br>訓練資料上傳模式在行駛時也能運作,但需連接至行動熱點或使用不限流量的 SIM 卡。<br><br><br><b>常見問題</b><br><br><i>我開車的方式或地點有影響嗎?</i> 不會,請像平常一樣駕駛即可。<br><br><i>訓練資料上傳模式會上傳所有的駕駛片段嗎?</i>不會,我們會選擇性地上傳部分片段。<br><br><i>什麼是好的 USB-C 充電器?</i>任何快速手機或筆電充電器都應該適用。<br><br><i>我使用的軟體版本有影響嗎?</i>有的,只有官方 sunnypilot(以及特定的分支)可以用於訓練。 - <b>%n segment(s)</b> of your driving is in the training dataset so far. <b>目前已有 %n 段</b> 您的駕駛數據被納入訓練資料集。 + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>閒置中</span>:請連接到不按流量計費的網絡 + + + Firehose Mode + Firehose 模式 + sunnypilot learns to drive by watching humans, like you, drive. @@ -625,11 +629,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>閒置中</span>:請連接到不按流量計費的網絡 - - - Firehose Mode + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -737,6 +737,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -753,14 +761,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform supports limited MADS settings. - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - LongitudinalPanel @@ -792,11 +792,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -807,18 +811,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -835,6 +827,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -847,6 +843,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -893,66 +893,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECT 選取 - - downloaded - - - - ready - - - - from cache - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - 重設校準 - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - 繼續 - - - on Metered - - - - Cancel - 取消 - Clear Model Cache @@ -994,6 +934,18 @@ The default software delay value is 0.2 %1 - %2 + + downloaded + + + + ready + + + + from cache + + download failed - %1 @@ -1002,12 +954,40 @@ The default software delay value is 0.2 pending - %1 + + Fetching models... + + + + Select a Model + + + + Default + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad. - Current: + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + 重設校準 + + + Driving Model Selector @@ -1018,6 +998,22 @@ The default software delay value is 0.2 Clear Cache + + Warning: You are on a metered connection! + + + + Continue + 繼續 + + + on Metered + + + + Cancel + 取消 + MultiOptionDialog @@ -1121,14 +1117,6 @@ The default software delay value is 0.2 OffroadAlert - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - 請立即連接網路檢查更新。如果不連接網路,sunnypilot 將在 %1 後便無法使用 - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - 請連接至網際網路以檢查更新。在連接至網際網路並完成更新檢查之前,sunnypilot 將不會自動啟動。 - Unable to download updates %1 @@ -1143,29 +1131,45 @@ The default software delay value is 0.2 An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. 一個有關操作系統的更新正在後台下載中。當更新準備好安裝時,您將收到提示進行更新。 - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - 裝置註冊失敗。它將無法連接或上傳至 comma.ai 伺服器,並且無法獲得 comma.ai 的支援。如果這是一個官方裝置,請訪問 https://comma.ai/support 。 - NVMe drive not mounted. NVMe 固態硬碟未被掛載。 - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - 檢測到不支援的 NVMe 固態硬碟。您的裝置因為使用了不支援的 NVMe 固態硬碟可能會消耗更多電力並更易過熱。 + Device temperature too high. System cooling down before starting. Current internal component temperature: %1 + 裝置溫度過高。系統正在冷卻中,等冷卻完畢後才會啟動。目前內部組件溫度:%1 + + + Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. + 裝置註冊 comma.ai 後端失敗。此裝置將無法連線或上傳資料至 comma.ai 伺服器,也無法獲得 comma.ai 的支援。若此裝置購自 comma.ai/shop,請至 https://comma.ai/support 建立支援請求。 + + + Acknowledge Excessive Actuation + + + + Snooze Update + 暫停更新 + + + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - sunnypilot 無法識別您的車輛。您的車輛可能未被支援,或是其電控單元 (ECU) 未被識別。請提交一個 Pull Request 為您的車輛添加正確的韌體版本。需要幫助嗎?請加入 discord.comma.ai 。 + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilot 偵測到裝置的安裝位置發生變化。請確保裝置完全安裝在支架上,並確保支架牢固地固定在擋風玻璃上。 - - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - 裝置溫度過高。系統正在冷卻中,等冷卻完畢後才會啟動。目前內部組件溫度:%1 + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. @@ -1198,10 +1202,6 @@ The default software delay value is 0.2 OnroadAlerts - - sunnypilot Unavailable - 無法使用 sunnypilot - TAKE CONTROL IMMEDIATELY 立即接管 @@ -1218,6 +1218,10 @@ The default software delay value is 0.2 System Unresponsive 系統無回應 + + sunnypilot Unavailable + + OsmPanel @@ -1435,7 +1439,7 @@ Warning: You are on a metered connection! Confirm - 確認 + Cancel @@ -1518,10 +1522,6 @@ Warning: You are on a metered connection! QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1544,46 +1544,9 @@ Warning: You are on a metered connection! now 現在 - - - Reset - Reset failed. Reboot to try again. - 重設失敗。請重新啟動後再試。 - - - Are you sure you want to reset your device? - 您確定要重設你的裝置嗎? - - - System Reset - 系統重設 - - - Cancel - 取消 - - - Reboot - 重新啟動 - - - Confirm - 確認 - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - 無法掛載資料分割區。分割區可能已經毀損。請確認是否要刪除並重置。 - - - Resetting device... -This may take up to a minute. - 重置中… -這可能需要一分鐘的時間。 - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - 系統重設已啟動。按下「確認」以清除所有內容和設定,或按下「取消」以繼續開機。 + sunnypilot + @@ -1614,7 +1577,7 @@ This may take up to a minute. Firehose - 訓練上傳 + Firehose @@ -1644,29 +1607,13 @@ This may take up to a minute. 軟體 - Trips + Models - - Vehicle - - - - Developer - 開發人員 - - - Firehose - 訓練上傳 - Steering - - Models - - Cruise @@ -1679,111 +1626,22 @@ This may take up to a minute. OSM - - - Setup - WARNING: Low Voltage - 警告:電壓過低 - - - Power your device in a car with a harness or proceed at your own risk. - 請使用車上 harness 提供的電源,若繼續的話您需要自擔風險。 - - - Power off - 關機 - - - Continue - 繼續 - - - Getting Started - 入門 - - - Before we get on the road, let’s finish installation and cover some details. - 在我們上路之前,讓我們完成安裝並介紹一些細節。 - - - Connect to Wi-Fi - 連接到無線網路 - - - Back - 回上頁 - - - Continue without Wi-Fi - 在沒有 Wi-Fi 的情況下繼續 - - - Waiting for internet - 連接至網路中 - - - Enter URL - 輸入網址 - - - for Custom Software - 訂製的軟體 - - - Downloading... - 下載中… - - - Download Failed - 下載失敗 - - - Ensure the entered URL is valid, and the device’s internet connection is good. - 請確定您輸入的是有效的安裝網址,並且確定裝置的網路連線狀態良好。 - - - Reboot device - 重新啟動 - - - Start over - 重新開始 - - - No custom software found at this URL. - 在此網址找不到自訂軟體。 - - - Something went wrong. Reboot the device. - 發生了一些錯誤。請重新啟動您的裝置。 - - - Select a language - 選擇語言 - - - Choose Software to Install - 選擇要安裝的軟體 - - - sunnypilot - sunnypilot - - - Custom Software - 自訂軟體 - - - WARNING: Custom Software + Trips - Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. - -If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + Vehicle + + Firehose + Firehose + + + Developer + 開發人員 + SetupWidget @@ -1987,14 +1845,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enter search keywords, or leave blank to list all branches. - - No branches found for keywords: %1 - - - - Select a branch - 選取一個分支 - Disable Updates @@ -2003,6 +1853,14 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 + + + + Select a branch + 選取一個分支 + %1 updates requires a reboot.<br>Reboot now? @@ -2076,22 +1934,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - 無法使用 - Sponsor Status @@ -2116,24 +1958,20 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + 無法使用 + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -2188,6 +2026,26 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -2240,11 +2098,11 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Welcome to sunnypilot - 歡迎使用 sunnypilot + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - 您必須接受《條款與條件》才能使用 sunnypilot。在繼續之前,請先閱讀最新條款:<span style='color: #465BEA;'>https://comma.ai/terms</span>。 + @@ -2277,22 +2135,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Disengage on Accelerator Pedal 油門取消控車 - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - 啟用後,踩踏油門將會取消 sunnypilot 控制。 - Experimental Mode 實驗模式 - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - sunnypilot 預設以 <b>輕鬆模式</b> 駕駛。 實驗模式啟用了尚未準備好進入輕鬆模式的 <b>alpha 級功能</b>。實驗功能如下: - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - 讓駕駛模型來控制油門及煞車。sunnypilot將會模擬人類的駕駛行為,包含在看見紅燈及停止標示時停車。由於車速將由駕駛模型決定,因此您設定的時速將成為速度上限。本功能仍在早期實驗階段,請預期模型有犯錯的可能性。 - New Driving Visualization 新的駕駛視覺介面 @@ -2301,6 +2147,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. 因車輛使用內建ACC系統,無法在本車輛上啟動實驗模式。 + + openpilot longitudinal control may come in a future update. + openpilot 縱向控制可能會在未來的更新中提供。 + Aggressive 積極 @@ -2317,22 +2167,10 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Driving Personality 駕駛風格 - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - 在正式 (release) 版以外的分支上可以測試 sunnypilot 縱向控制的 Alpha 版本以及實驗模式。 - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - 啟用 sunnypilot 縱向控制(alpha)切換以允許實驗模式。 - End-to-End Longitudinal Control 端到端縱向控制 - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - 建議使用標準模式。在積極模式下,sunnypilot 會更接近前車並更積極地使用油門和剎車。在輕鬆模式下,sunnypilot 會與前車保持較遠距離。對於支援的汽車,您可以使用方向盤上的距離按鈕來切換這些駕駛風格。 - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. 在低速時,駕駛可視化將切換至道路朝向的廣角攝影機,以更好地顯示某些彎道。在右上角還會顯示「實驗模式」的標誌。 @@ -2342,8 +2180,34 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device 駕駛監控常開 - Enable driver monitoring even when sunnypilot is not engaged. - 即使在sunnypilot未激活時也啟用駕駛監控。 + Changing this setting will restart openpilot if the car is powered on. + 若車輛電源為開啟狀態,變更此設定將會重新啟動 openpilot。 + + + Record and Upload Microphone Audio + 錄製並上傳麥克風音訊 + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + 在駕駛時錄製並儲存麥克風音訊。此音訊將會收錄在 comma connect 的行車記錄器影片中。 + + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2354,27 +2218,31 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device - Enable sunnypilot - 啟用 sunnypilot - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Record and Upload Microphone Audio + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. @@ -2389,41 +2257,6 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device 取消 - - Updater - - Update Required - 系統更新 - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - 需要進行作業系統更新。建議將您的裝置連接上 Wi-Fi 獲得更快的更新下載。下載大小約為 1GB。 - - - Connect to Wi-Fi - 連接到無線網路 - - - Install - 安裝 - - - Back - 回上頁 - - - Loading... - 載入中… - - - Reboot - 重新啟動 - - - Update failed - 更新失敗 - - VisualsPanel @@ -2436,7 +2269,7 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Changing this setting will restart openpilot if the car is powered on. - + 若車輛電源為開啟狀態,變更此設定將會重新啟動 openpilot。 Off @@ -2474,12 +2307,12 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device 開啟 - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> 訓練資料上傳模式 <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. + 最大化您的訓練數據上傳,以改善 openpilot 的駕駛模型。 - Maximize your training data uploads to improve openpilot's driving models. - + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose 模式 <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/watch3.cc b/selfdrive/ui/watch3.cc deleted file mode 100644 index 258e2a7bd6..0000000000 --- a/selfdrive/ui/watch3.cc +++ /dev/null @@ -1,33 +0,0 @@ -#include -#include - -#include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/widgets/cameraview.h" - -int main(int argc, char *argv[]) { - initApp(argc, argv); - - QApplication a(argc, argv); - QWidget w; - setMainWindow(&w); - - QVBoxLayout *layout = new QVBoxLayout(&w); - layout->setMargin(0); - layout->setSpacing(0); - - { - QHBoxLayout *hlayout = new QHBoxLayout(); - layout->addLayout(hlayout); - hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_ROAD)); - } - - { - QHBoxLayout *hlayout = new QHBoxLayout(); - layout->addLayout(hlayout); - hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_DRIVER)); - hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_WIDE_ROAD)); - } - - return a.exec(); -} diff --git a/selfdrive/ui/watch3.py b/selfdrive/ui/watch3.py new file mode 100755 index 0000000000..bb64cdc4d5 --- /dev/null +++ b/selfdrive/ui/watch3.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +import pyray as rl + +from msgq.visionipc import VisionStreamType +from openpilot.system.ui.lib.application import gui_app +from openpilot.selfdrive.ui.onroad.cameraview import CameraView + + +if __name__ == "__main__": + gui_app.init_window("watch3") + road = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) + driver = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER) + wide = CameraView("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD) + for _ in gui_app.render(): + road.render(rl.Rectangle(gui_app.width // 4, 0, gui_app.width // 2, gui_app.height // 2)) + driver.render(rl.Rectangle(0, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2)) + wide.render(rl.Rectangle(gui_app.width // 2, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2)) diff --git a/selfdrive/ui/widgets/exp_mode_button.py b/selfdrive/ui/widgets/exp_mode_button.py index 83caf413d1..88664e0fad 100644 --- a/selfdrive/ui/widgets/exp_mode_button.py +++ b/selfdrive/ui/widgets/exp_mode_button.py @@ -1,7 +1,7 @@ import pyray as rl from openpilot.common.params import Params from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget class ExperimentalModeButton(Widget): @@ -14,7 +14,6 @@ class ExperimentalModeButton(Widget): self.params = Params() self.experimental_mode = self.params.get_bool("ExperimentalMode") - self.is_pressed = False self.chill_pixmap = gui_app.texture("icons/couch.png", self.img_width, self.img_width) self.experimental_pixmap = gui_app.texture("icons/experimental_grey.png", self.img_width, self.img_width) @@ -32,19 +31,12 @@ class ExperimentalModeButton(Widget): rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), int(rect.width), int(rect.height), start_color, end_color) - def _handle_interaction(self, rect): - mouse_pos = rl.get_mouse_position() - mouse_in_rect = rl.check_collision_point_rec(mouse_pos, rect) - - self.is_pressed = mouse_in_rect and rl.is_mouse_button_down(rl.MOUSE_BUTTON_LEFT) - return mouse_in_rect and rl.is_mouse_button_released(rl.MOUSE_BUTTON_LEFT) + def _handle_mouse_release(self, mouse_pos): + self.experimental_mode = not self.experimental_mode + # TODO: Opening settings for ExperimentalMode + self.params.put_bool("ExperimentalMode", self.experimental_mode) def _render(self, rect): - if self._handle_interaction(rect): - self.experimental_mode = not self.experimental_mode - # TODO: Opening settings for ExperimentalMode - self.params.put_bool("ExperimentalMode", self.experimental_mode) - rl.draw_rectangle_rounded(rect, 0.08, 20, rl.Color(255, 255, 255, 255)) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) diff --git a/selfdrive/ui/widgets/offroad_alerts.py b/selfdrive/ui/widgets/offroad_alerts.py index aba07139db..f54bd9116c 100644 --- a/selfdrive/ui/widgets/offroad_alerts.py +++ b/selfdrive/ui/widgets/offroad_alerts.py @@ -5,11 +5,11 @@ from collections.abc import Callable from dataclasses import dataclass from openpilot.common.params import Params from openpilot.system.hardware import HARDWARE -from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.wrap_text import wrap_text -from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.widgets import Widget class AlertColors: @@ -190,14 +190,10 @@ class OffroadAlert(AbstractAlert): for alert_data in self.sorted_alerts: text = "" - bytes_data = self.params.get(alert_data.key) + alert_json = self.params.get(alert_data.key) - if bytes_data: - try: - alert_json = json.loads(bytes_data) - text = alert_json.get("text", "").replace("{}", alert_json.get("extra", "")) - except json.JSONDecodeError: - text = "" + if alert_json: + text = alert_json.get("text", "").replace("{}", alert_json.get("extra", "")) alert_data.text = text alert_data.visible = bool(text) @@ -296,7 +292,7 @@ class UpdateAlert(AbstractAlert): def refresh(self) -> bool: update_available: bool = self.params.get_bool("UpdateAvailable") if update_available: - self.release_notes = self.params.get("UpdaterNewReleaseNotes", encoding='utf-8') + self.release_notes = self.params.get("UpdaterNewReleaseNotes") self._cached_content_height = 0 return update_available diff --git a/selfdrive/ui/widgets/pairing_dialog.py b/selfdrive/ui/widgets/pairing_dialog.py index 2cb1499499..63298914ea 100644 --- a/selfdrive/ui/widgets/pairing_dialog.py +++ b/selfdrive/ui/widgets/pairing_dialog.py @@ -23,7 +23,7 @@ class PairingDialog: def _get_pairing_url(self) -> str: try: - dongle_id = self.params.get("DongleId", encoding='utf8') or "" + dongle_id = self.params.get("DongleId") or "" token = Api(dongle_id).get_token() except Exception as e: cloudlog.warning(f"Failed to get pairing token: {e}") @@ -55,7 +55,7 @@ class PairingDialog: self.qr_texture = None def _check_qr_refresh(self) -> None: - current_time = time.time() + current_time = time.monotonic() if current_time - self.last_qr_generation >= self.QR_REFRESH_INTERVAL: self._generate_qr_code() self.last_qr_generation = current_time diff --git a/selfdrive/ui/widgets/prime.py b/selfdrive/ui/widgets/prime.py index afdfa33e7a..86234ee149 100644 --- a/selfdrive/ui/widgets/prime.py +++ b/selfdrive/ui/widgets/prime.py @@ -2,10 +2,10 @@ import pyray as rl from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.label import gui_label -from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.label import gui_label class PrimeWidget(Widget): diff --git a/selfdrive/ui/widgets/setup.py b/selfdrive/ui/widgets/setup.py index 4fd56aa522..35a7c4101c 100644 --- a/selfdrive/ui/widgets/setup.py +++ b/selfdrive/ui/widgets/setup.py @@ -1,11 +1,11 @@ import pyray as rl from openpilot.selfdrive.ui.lib.prime_state import PrimeType -from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.wrap_text import wrap_text -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle class SetupWidget(Widget): diff --git a/selfdrive/ui/widgets/ssh_key.py b/selfdrive/ui/widgets/ssh_key.py index 418867c86f..4f4a8dcdff 100644 --- a/selfdrive/ui/widgets/ssh_key.py +++ b/selfdrive/ui/widgets/ssh_key.py @@ -6,8 +6,12 @@ from enum import Enum from openpilot.common.params import Params from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.list_view import ( +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets import DialogResult +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle +from openpilot.system.ui.widgets.confirm_dialog import alert_dialog +from openpilot.system.ui.widgets.keyboard import Keyboard +from openpilot.system.ui.widgets.list_view import ( ItemAction, ListItem, BUTTON_HEIGHT, @@ -15,10 +19,6 @@ from openpilot.system.ui.lib.list_view import ( BUTTON_FONT_SIZE, BUTTON_WIDTH, ) -from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.widget import DialogResult -from openpilot.system.ui.widgets.confirm_dialog import alert_dialog -from openpilot.system.ui.widgets.keyboard import Keyboard class SshKeyActionState(Enum): @@ -42,7 +42,7 @@ class SshKeyAction(ItemAction): self._refresh_state() def _refresh_state(self): - self._username = self._params.get("GithubUsername", "") + self._username = self._params.get("GithubUsername") self._state = SshKeyActionState.REMOVE if self._params.get("GithubSshKeys") else SshKeyActionState.ADD def _render(self, rect: rl.Rectangle) -> bool: diff --git a/sunnypilot/livedelay/helpers.py b/sunnypilot/livedelay/helpers.py new file mode 100644 index 0000000000..0f7437ceea --- /dev/null +++ b/sunnypilot/livedelay/helpers.py @@ -0,0 +1,14 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from openpilot.common.params import Params + + +def get_lat_delay(params: Params, stock_lat_delay: float) -> float: + if params.get_bool("LagdToggle"): + return float(params.get("LagdValueCache", return_default=True)) + + return stock_lat_delay diff --git a/sunnypilot/livedelay/lagd_toggle.py b/sunnypilot/livedelay/lagd_toggle.py index cc2f1e2504..8495dcee0a 100644 --- a/sunnypilot/livedelay/lagd_toggle.py +++ b/sunnypilot/livedelay/lagd_toggle.py @@ -4,48 +4,35 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +from cereal import log + +from opendbc.car import structs from openpilot.common.params import Params -from openpilot.common.swaglog import cloudlog class LagdToggle: - def __init__(self): + def __init__(self, CP: structs.CarParams): + self.CP = CP self.params = Params() self.lag = 0.0 - self._last_desc = None - @property - def software_delay(self): - return float(self.params.get("LagdToggledelay", encoding='utf8')) + self.lagd_toggle = self.params.get_bool("LagdToggle") + self.software_delay = self.params.get("LagdToggleDelay", return_default=True) - def _maybe_update_desc(self, desc): - if desc != self._last_desc: - self.params.put_nonblocking("LagdToggleDesc", desc) - self._last_desc = desc + def read_params(self) -> None: + self.lagd_toggle = self.params.get_bool("LagdToggle") + self.software_delay = self.params.get("LagdToggleDelay", return_default=True) - def lagd_main(self, CP, sm, model): - if self.params.get_bool("LagdToggle"): - lateral_delay = sm["liveDelay"].lateralDelay - lat_smooth = model.LAT_SMOOTH_SECONDS - result = lateral_delay + lat_smooth - desc = f"live steer delay learner ({lateral_delay:.3f}s) + model smoothing filter ({lat_smooth:.3f}s) = total delay ({result:.3f}s)" - self._maybe_update_desc(desc) - return result + def update(self, lag_msg: log.LiveDelayData) -> None: + self.read_params() - steer_actuator_delay = CP.steerActuatorDelay - lat_smooth = model.LAT_SMOOTH_SECONDS - delay = self.software_delay - result = (steer_actuator_delay + delay) + lat_smooth - desc = (f"Vehicle steering delay ({steer_actuator_delay:.3f}s) + software delay ({delay:.3f}s) + " + - f"model smoothing filter ({lat_smooth:.3f}s) = total delay ({result:.3f}s)") - self._maybe_update_desc(desc) - return result + if not self.lagd_toggle: + steer_actuator_delay = self.CP.steerActuatorDelay + delay = self.software_delay + self.lag = (steer_actuator_delay + delay) + self.params.put_nonblocking("LagdValueCache", self.lag) + return - def lagd_torqued_main(self, CP, msg): - if self.params.get_bool("LagdToggle"): - self.lag = msg.lateralDelay - cloudlog.debug(f"TORQUED USING LIVE DELAY: {self.lag:.3f}") - else: - self.lag = CP.steerActuatorDelay + self.software_delay - cloudlog.debug(f"TORQUED USING STEER ACTUATOR: {self.lag:.3f}") - return self.lag + lateral_delay = lag_msg.liveDelay.lateralDelay + self.lag = lateral_delay + self.params.put_nonblocking("LagdValueCache", self.lag) diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 4b1095c63e..70b590a8b2 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -25,10 +25,7 @@ def read_steering_mode_param(CP: structs.CarParams, params: Params): if get_mads_limited_brands(CP): return MadsSteeringModeOnBrake.DISENGAGE - try: - return int(params.get("MadsSteeringMode")) - except (ValueError, TypeError): - return MadsSteeringModeOnBrake.REMAIN_ACTIVE + return params.get("MadsSteeringMode", return_default=True) def set_alternative_experience(CP: structs.CarParams, params: Params): @@ -59,6 +56,6 @@ def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, p # TODO-SP: To enable MADS full support for Rivian/Tesla, identify consistent signals for MADS toggling mads_partial_support = get_mads_limited_brands(CP) if mads_partial_support: - params.put("MadsSteeringMode", "2") + params.put("MadsSteeringMode", 2) params.put_bool("MadsUnifiedEngagementMode", True) params.remove("MadsMainCruiseAllowed") diff --git a/sunnypilot/mapd/live_map_data/osm_map_data.py b/sunnypilot/mapd/live_map_data/osm_map_data.py index 5f74a5d011..beaa785f63 100644 --- a/sunnypilot/mapd/live_map_data/osm_map_data.py +++ b/sunnypilot/mapd/live_map_data/osm_map_data.py @@ -31,14 +31,14 @@ class OsmMapData(BaseMapData): self.mem_params.put("LastGPSPosition", json.dumps(params)) def get_current_speed_limit(self) -> float: - return float(self.mem_params.get("MapSpeedLimit", encoding='utf8') or 0.0) + return float(self.mem_params.get("MapSpeedLimit") or 0.0) def get_current_road_name(self) -> str: - return self.mem_params.get("RoadName", encoding='utf8') or "" + return str(self.mem_params.get("RoadName")) def get_next_speed_limit_and_distance(self) -> tuple[float, float]: - next_speed_limit_section_str = self.mem_params.get("NextMapSpeedLimit", encoding='utf8') - next_speed_limit_section = json.loads(next_speed_limit_section_str) if next_speed_limit_section_str else {} + next_speed_limit_section_str = self.mem_params.get("NextMapSpeedLimit") + next_speed_limit_section = next_speed_limit_section_str if next_speed_limit_section_str else {} next_speed_limit = next_speed_limit_section.get('speedlimit', 0.0) next_speed_limit_latitude = next_speed_limit_section.get('latitude') next_speed_limit_longitude = next_speed_limit_section.get('longitude') diff --git a/sunnypilot/mapd/mapd_installer.py b/sunnypilot/mapd/mapd_installer.py index c337b50c5f..4cfb5ebbbf 100755 --- a/sunnypilot/mapd/mapd_installer.py +++ b/sunnypilot/mapd/mapd_installer.py @@ -90,7 +90,7 @@ class MapdInstallManager: logging.error("Failed to download file after all retries") def get_installed_version(self) -> str: - return self._params.get("MapdVersion", encoding="utf-8") or "" + return str(self._params.get("MapdVersion")) def wait_for_internet_connection(self, return_on_failure: bool = False) -> bool: max_retries = 10 diff --git a/sunnypilot/mapd/mapd_manager.py b/sunnypilot/mapd/mapd_manager.py index 0cb0df23f0..9ebf07a7f4 100755 --- a/sunnypilot/mapd/mapd_manager.py +++ b/sunnypilot/mapd/mapd_manager.py @@ -56,15 +56,20 @@ def cleanup_old_osm_data(files_to_remove: list[str]) -> None: def request_refresh_osm_location_data(nations: list[str], states: list[str] = None) -> None: - params.put("OsmDownloadedDate", str(time.time())) + params.put("OsmDownloadedDate", str(time.monotonic())) params.put_bool("OsmDbUpdatesCheck", False) - osm_download_locations = json.dumps({ + osm_download_locations = { + "nations": nations, + "states": states or [] + } + + osm_download_locations_dump = json.dumps({ "nations": nations, "states": states or [] }) - print(f"Downloading maps for {osm_download_locations}") + print(f"Downloading maps for {osm_download_locations_dump}") mem_params.put("OSMDownloadLocations", osm_download_locations) @@ -98,12 +103,12 @@ def filter_nations_and_states(nations: list[str], states: list[str] = None) -> t def update_osm_db() -> None: - # last_downloaded_date = float(params.get('OsmDownloadedDate', encoding='utf-8') or 0.0) - # if params.get_bool("OsmDbUpdatesCheck") or time.time() - last_downloaded_date >= 604800: # 7 days * 24 hours/day * 60 + # last_downloaded_date = params.get("OsmDownloadedDate", return_default=True) + # if params.get_bool("OsmDbUpdatesCheck") or time.monotonic() - last_downloaded_date >= 604800: # 7 days * 24 hours/day * 60 if params.get_bool("OsmDbUpdatesCheck"): cleanup_old_osm_data(get_files_for_cleanup()) - country = params.get('OsmLocationName', encoding='utf-8') - state = params.get('OsmStateName', encoding='utf-8') or "All" + country = params.get("OsmLocationName", return_default=True) + state = params.get("OsmStateName", return_default=True) filtered_nations, filtered_states = filter_nations_and_states([country], [state]) request_refresh_osm_location_data(filtered_nations, filtered_states) diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py index 10725efb71..b94e166bc6 100755 --- a/sunnypilot/modeld/modeld.py +++ b/sunnypilot/modeld/modeld.py @@ -20,13 +20,14 @@ from openpilot.system import sentry from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value +from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.modeld.runners import ModelRunner, Runtime from openpilot.sunnypilot.modeld.parse_model_outputs import Parser from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan from openpilot.sunnypilot.models.helpers import get_active_bundle, get_model_path, load_metadata, prepare_inputs, load_meta_constants -from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext +from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase PROCESS_NAME = "selfdrive.modeld.modeld_snpe" @@ -48,7 +49,7 @@ class FrameMeta: if vipc is not None: self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof -class ModelState: +class ModelState(ModelStateBase): frame: ModelFrame wide_frame: ModelFrame inputs: dict[str, np.ndarray] @@ -57,6 +58,7 @@ class ModelState: model: ModelRunner def __init__(self, context: CLContext): + ModelStateBase.__init__(self) self.frame = ModelFrame(context) self.wide_frame = ModelFrame(context) self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) @@ -202,8 +204,6 @@ def main(demo=False): cloudlog.info("modeld got CarParams: %s", CP.brand) - modeld_lagd = LagdToggle() - # Enable lagd support for sunnypilot modeld long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS prev_action = log.ModelDataV2.Action() @@ -248,8 +248,9 @@ def main(demo=False): v_ego = sm["carState"].vEgo is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId - steer_delay = modeld_lagd.lagd_main(CP, sm, model) - + if sm.frame % 60 == 0: + model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay) + lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] @@ -283,7 +284,7 @@ def main(demo=False): } if "lateral_control_params" in model.inputs.keys(): - inputs['lateral_control_params'] = np.array([max(v_ego, 0.), steer_delay], dtype=np.float32) + inputs['lateral_control_params'] = np.array([max(v_ego, 0.), lat_delay], dtype=np.float32) if "driving_style" in model.inputs.keys(): inputs['driving_style'] = np.array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], dtype=np.float32) @@ -306,7 +307,7 @@ def main(demo=False): action = model.get_action_from_model(model_output, prev_action, long_delay + DT_MDL) fill_model_msg(drivingdata_send, modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, - v_ego, steer_delay, model.meta) + v_ego, lat_delay, model.meta) desire_state = modelv2_send.modelV2.meta.desireState l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] diff --git a/sunnypilot/modeld/modeld_base.py b/sunnypilot/modeld/modeld_base.py new file mode 100644 index 0000000000..ba57659b09 --- /dev/null +++ b/sunnypilot/modeld/modeld_base.py @@ -0,0 +1,12 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from openpilot.common.params import Params + + +class ModelStateBase: + def __init__(self): + self.lat_delay = Params().get("LagdValueCache", return_default=True) diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index 93a8a3f05e..2dc1026c01 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -22,8 +22,9 @@ from openpilot.sunnypilot.modeld_v2.constants import Plan from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLContext from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants +from openpilot.sunnypilot.livedelay.helpers import get_lat_delay +from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase from openpilot.sunnypilot.models.helpers import get_active_bundle -from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle from openpilot.sunnypilot.models.runners.helpers import get_model_runner PROCESS_NAME = "selfdrive.modeld.modeld" @@ -39,13 +40,14 @@ class FrameMeta: self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof -class ModelState: +class ModelState(ModelStateBase): frames: dict[str, DrivingModelFrame] inputs: dict[str, np.ndarray] prev_desire: np.ndarray # for tracking the rising edge of the pulse temporal_idxs: slice | np.ndarray def __init__(self, context: CLContext): + ModelStateBase.__init__(self) try: self.model_runner = get_model_runner() self.constants = self.model_runner.constants @@ -242,9 +244,6 @@ def main(demo=False): CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) cloudlog.info("modeld got CarParams: %s", CP.brand) - - modeld_lagd = LagdToggle() - # TODO Move smooth seconds to action function long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS prev_action = log.ModelDataV2.Action() @@ -289,7 +288,9 @@ def main(demo=False): is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId v_ego = max(sm["carState"].vEgo, 0.) - steer_delay = modeld_lagd.lagd_main(CP, sm, model) + if sm.frame % 60 == 0: + model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay) + lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] @@ -326,7 +327,7 @@ def main(demo=False): } if "lateral_control_params" in model.numpy_inputs.keys(): - inputs['lateral_control_params'] = np.array([v_ego, steer_delay], dtype=np.float32) + inputs['lateral_control_params'] = np.array([v_ego, lat_delay], dtype=np.float32) mt1 = time.perf_counter() model_output = model.run(bufs, transforms, inputs, prepare_only) @@ -338,7 +339,7 @@ def main(demo=False): drivingdata_send = messaging.new_message('drivingModelData') posenet_send = messaging.new_message('cameraOdometry') - action = model.get_action_from_model(model_output, prev_action, steer_delay + DT_MDL, long_delay + DT_MDL, v_ego) + action = model.get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego) prev_action = action fill_model_msg(drivingdata_send, modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index 9d0f4e318d..482f0e3bb1 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -5,7 +5,6 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -import json import time import requests @@ -88,8 +87,8 @@ class ModelCache: def _is_expired(self) -> bool: """Checks if the cache has expired""" current_time = int(time.monotonic() * 1e9) - last_sync = int(self.params.get(self._LAST_SYNC_KEY, encoding="utf-8") or 0) - return last_sync == 0 or (current_time - last_sync) >= self.cache_timeout + last_sync = self.params.get(self._LAST_SYNC_KEY) or 0 + return bool(last_sync == 0) or (current_time - last_sync) >= self.cache_timeout def get(self) -> tuple[dict, bool]: """ @@ -98,19 +97,19 @@ class ModelCache: If no cached data exists or on error, returns an empty dict """ try: - cached_data = self.params.get(self._CACHE_KEY, encoding="utf-8") + cached_data = self.params.get(self._CACHE_KEY) if not cached_data: cloudlog.warning("No cached model data available") return {}, True - return json.loads(cached_data), self._is_expired() + return cached_data, self._is_expired() except Exception as e: cloudlog.exception(f"Error retrieving cached model data: {str(e)}") return {}, True def set(self, data: dict) -> None: """Updates the cache with new model data""" - self.params.put(self._CACHE_KEY, json.dumps(data)) - self.params.put(self._LAST_SYNC_KEY, str(int(time.monotonic() * 1e9))) + self.params.put(self._CACHE_KEY, data) + self.params.put(self._LAST_SYNC_KEY, int(time.monotonic() * 1e9)) class ModelFetcher: diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index 01069cddb4..a024935b84 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -9,7 +9,6 @@ import hashlib import os import pickle import numpy as np -import json from openpilot.common.params import Params from cereal import custom @@ -64,13 +63,14 @@ def is_bundle_version_compatible(bundle: dict) -> bool: """ return bool(REQUIRED_MIN_SELECTOR_VERSION <= bundle.get("minimumSelectorVersion", 0) <= CURRENT_SELECTOR_VERSION) + def get_active_bundle(params: Params = None) -> custom.ModelManagerSP.ModelBundle: """Gets the active model bundle from cache""" if params is None: params = Params() try: - if (active_bundle := json.loads(params.get("ModelManager_ActiveBundle") or "{}")) and is_bundle_version_compatible(active_bundle): + if (active_bundle := params.get("ModelManager_ActiveBundle") or {}) and is_bundle_version_compatible(active_bundle): return custom.ModelManagerSP.ModelBundle(**active_bundle) except Exception: pass @@ -111,7 +111,7 @@ def get_active_model_runner(params: Params = None, force_check=False) -> custom. runner_type = active_bundle.runner.raw if cached_runner_type != runner_type: - params.put("ModelRunnerTypeCache", str(int(runner_type))) + params.put("ModelRunnerTypeCache", int(runner_type)) return runner_type diff --git a/sunnypilot/models/manager.py b/sunnypilot/models/manager.py index 8913135203..a3ce046a33 100644 --- a/sunnypilot/models/manager.py +++ b/sunnypilot/models/manager.py @@ -8,7 +8,6 @@ See the LICENSE.md file in the root directory for more details. import asyncio import os import time -import json import aiohttp from openpilot.common.params import Params @@ -146,7 +145,7 @@ class ModelManagerSP: await asyncio.gather(*tasks) self.active_bundle = self.selected_bundle self.active_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded - self.params.put("ModelManager_ActiveBundle", json.dumps(self.active_bundle.to_dict())) + self.params.put("ModelManager_ActiveBundle", self.active_bundle.to_dict()) self.selected_bundle = None except Exception: @@ -169,16 +168,16 @@ class ModelManagerSP: self.available_models = self.model_fetcher.get_available_bundles() self.active_bundle = get_active_bundle(self.params) - if index_to_download := self.params.get("ModelManager_DownloadIndex", block=False, encoding="utf-8"): - if model_to_download := next((model for model in self.available_models if model.index == int(index_to_download)), None): + if index_to_download := self.params.get("ModelManager_DownloadIndex"): + if model_to_download := next((model for model in self.available_models if model.index == index_to_download), None): try: self.download(model_to_download, Paths.model_root()) except Exception as e: cloudlog.exception(e) finally: - self.params.put("ModelManager_DownloadIndex", "") + self.params.remove("ModelManager_DownloadIndex") - if self.params.get("ModelManager_ClearCache", block=False, encoding="utf-8"): + if self.params.get("ModelManager_ClearCache"): self.clear_model_cache() self.params.remove("ModelManager_ClearCache") diff --git a/sunnypilot/models/tests/model_hash b/sunnypilot/models/tests/model_hash index 6df03c31d1..d104e049b7 100644 --- a/sunnypilot/models/tests/model_hash +++ b/sunnypilot/models/tests/model_hash @@ -1 +1 @@ -61bb1f3077ef8219a44c8c627d5345d1a96e7f859b850bed6215eec91090becb \ No newline at end of file +71c14a02af81857565e5fcffaaddb57a76fe80b283fe3292da830dbc92e211b7 \ No newline at end of file diff --git a/sunnypilot/models/tests/test_tinygrad_ref.py b/sunnypilot/models/tests/test_tinygrad_ref.py new file mode 100644 index 0000000000..c4f98a2cb6 --- /dev/null +++ b/sunnypilot/models/tests/test_tinygrad_ref.py @@ -0,0 +1,23 @@ +import requests + +from sunnypilot.models.tinygrad_ref import get_tinygrad_ref +from sunnypilot.models.fetcher import ModelFetcher + + +def fetch_tinygrad_ref(): + response = requests.get(ModelFetcher.MODEL_URL, timeout=10) + response.raise_for_status() + json_data = response.json() + return json_data.get("tinygrad_ref") + + +def test_tinygrad_ref(): + current_ref = get_tinygrad_ref() + remote_ref = fetch_tinygrad_ref() + assert remote_ref == current_ref, ( + f"""tinygrad_repo ref does not match remote tinygrad_ref of current compiled driving models json. + Current: {current_ref} + Remote: {remote_ref} + Please run build-all workflow to update models.""" + ) + print("tinygrad_repo ref matches current compiled driving models json ref.") diff --git a/sunnypilot/models/tinygrad_ref.py b/sunnypilot/models/tinygrad_ref.py new file mode 100644 index 0000000000..4dd333e323 --- /dev/null +++ b/sunnypilot/models/tinygrad_ref.py @@ -0,0 +1,36 @@ +import os + +from openpilot.common.basedir import BASEDIR + + +def get_tinygrad_ref(): + repo_path = os.path.join(BASEDIR, "tinygrad_repo") + git_path = os.path.join(repo_path, ".git") + try: + if os.path.isdir(git_path): + git_dir = git_path + else: + with open(git_path) as f: + line = f.read().strip() + git_dir = os.path.join(repo_path, line[8:]) + with open(os.path.join(git_dir, "HEAD")) as f: + ref = f.read().strip() + if ref.startswith("ref:"): + with open(os.path.join(git_dir, ref.split(" ", 1)[1])) as f: + return f.read().strip() + return ref + except Exception as e: + print(f"Error getting tinygrad_repo ref: {e}") + return None + + +def main(): + current_ref = get_tinygrad_ref() + if current_ref: + print(current_ref) + else: + print("") + + +if __name__ == "__main__": + main() diff --git a/sunnypilot/navd/helpers.py b/sunnypilot/navd/helpers.py index 85894fb2ab..c57706d32a 100644 --- a/sunnypilot/navd/helpers.py +++ b/sunnypilot/navd/helpers.py @@ -5,7 +5,7 @@ import math import numpy as np from typing import Any, cast -from openpilot.common.conversions import Conversions +from openpilot.common.constants import CV from openpilot.common.params import Params DIRECTIONS = ('left', 'right', 'straight') @@ -13,8 +13,8 @@ MODIFIABLE_DIRECTIONS = ('left', 'right') EARTH_MEAN_RADIUS = 6371007.2 SPEED_CONVERSIONS = { - 'km/h': Conversions.KPH_TO_MS, - 'mph': Conversions.MPH_TO_MS, + 'km/h': CV.KPH_TO_MS, + 'mph': CV.MPH_TO_MS, } @@ -133,7 +133,7 @@ def string_to_direction(direction: str) -> str: def maxspeed_to_ms(maxspeed: dict[str, str | float]) -> float: unit = cast(str, maxspeed['unit']) speed = cast(float, maxspeed['speed']) - return SPEED_CONVERSIONS[unit] * speed + return float(SPEED_CONVERSIONS[unit] * speed) def field_valid(dat: dict, field: str) -> bool: diff --git a/sunnypilot/selfdrive/car/cruise_ext.py b/sunnypilot/selfdrive/car/cruise_ext.py index 6f0419ed14..f443aeee0e 100644 --- a/sunnypilot/selfdrive/car/cruise_ext.py +++ b/sunnypilot/selfdrive/car/cruise_ext.py @@ -17,19 +17,13 @@ class VCruiseHelperSP: self.params = Params() self.custom_acc_enabled = self.params.get_bool("CustomAccIncrementsEnabled") - self.short_increment = self.read_int_param("CustomAccShortPressIncrement", 1) - self.long_increment = self.read_int_param("CustomAccLongPressIncrement", 5) - - def read_int_param(self, key: str, default: int = 0) -> int: - try: - return int(self.params.get(key, encoding='utf8')) - except (ValueError, TypeError): - return default + self.short_increment = self.params.get("CustomAccShortPressIncrement", return_default=True) + self.long_increment = self.params.get("CustomAccLongPressIncrement", return_default=True) def read_custom_set_speed_params(self) -> None: self.custom_acc_enabled = self.params.get_bool("CustomAccIncrementsEnabled") - self.short_increment = self.read_int_param("CustomAccShortPressIncrement", 1) - self.long_increment = self.read_int_param("CustomAccLongPressIncrement", 5) + self.short_increment = self.params.get("CustomAccShortPressIncrement", return_default=True) + self.long_increment = self.params.get("CustomAccLongPressIncrement", return_default=True) def update_v_cruise_delta(self, long_press: bool, v_cruise_delta: float) -> tuple[bool, float]: if not self.custom_acc_enabled: diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index e2a232a3ef..28bae4a0dd 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -30,7 +30,7 @@ def _initialize_custom_longitudinal_tuning(CI: CarInterfaceBase, CP: structs.Car # Hyundai Custom Longitudinal Tuning if CP.brand == 'hyundai': - hyundai_longitudinal_tuning = int(params.get("HyundaiLongitudinalTuning", encoding="utf8") or 0) + hyundai_longitudinal_tuning = params.get("HyundaiLongitudinalTuning") if hyundai_longitudinal_tuning == LongitudinalTuningType.DYNAMIC: CP_SP.flags |= HyundaiFlagsSP.LONG_TUNING_DYNAMIC.value if hyundai_longitudinal_tuning == LongitudinalTuningType.PREDICTIVE: diff --git a/sunnypilot/selfdrive/car/tests/test_custom_cruise.py b/sunnypilot/selfdrive/car/tests/test_custom_cruise.py index 2f9d976847..3bed8534ef 100644 --- a/sunnypilot/selfdrive/car/tests/test_custom_cruise.py +++ b/sunnypilot/selfdrive/car/tests/test_custom_cruise.py @@ -2,7 +2,7 @@ import pytest from parameterized import parameterized_class from cereal import car -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.selfdrive.car.cruise import V_CRUISE_INITIAL from openpilot.selfdrive.car.tests.test_cruise_speed import TestVCruiseHelper @@ -21,8 +21,8 @@ class TestCustomAccIncrements(TestVCruiseHelper): def reset_custom_params(self) -> None: """Reset to default custom ACC parameters""" self.params.put_bool("CustomAccIncrementsEnabled", False) - self.params.put("CustomAccShortPressIncrement", "1") - self.params.put("CustomAccLongPressIncrement", "5") + self.params.put("CustomAccShortPressIncrement", 1) + self.params.put("CustomAccLongPressIncrement", 5) self.v_cruise_helper.read_custom_set_speed_params() def press_button_short(self, button_type: car.CarState.ButtonEvent.Type) -> None: @@ -51,8 +51,8 @@ class TestCustomAccIncrements(TestVCruiseHelper): def set_custom_increments(self, enabled: bool, short_inc: int, long_inc: int) -> None: """Set custom ACC increment parameters""" self.params.put_bool("CustomAccIncrementsEnabled", enabled) - self.params.put("CustomAccShortPressIncrement", str(short_inc)) - self.params.put("CustomAccLongPressIncrement", str(long_inc)) + self.params.put("CustomAccShortPressIncrement", short_inc) + self.params.put("CustomAccLongPressIncrement", long_inc) self.v_cruise_helper.read_custom_set_speed_params() def test_default_behavior_when_disabled(self): diff --git a/sunnypilot/selfdrive/controls/lib/auto_lane_change.py b/sunnypilot/selfdrive/controls/lib/auto_lane_change.py index 29056d5ac5..cf8de2a1b2 100644 --- a/sunnypilot/selfdrive/controls/lib/auto_lane_change.py +++ b/sunnypilot/selfdrive/controls/lib/auto_lane_change.py @@ -42,7 +42,7 @@ class AutoLaneChangeController: self.param_read_counter = 0 self.lane_change_delay = 0.0 - self.lane_change_set_timer = AutoLaneChangeMode.NUDGE + self.lane_change_set_timer = self.params.get("AutoLaneChangeTimer", return_default=True) self.lane_change_bsm_delay = False self.prev_brake_pressed = False @@ -61,10 +61,7 @@ class AutoLaneChangeController: def read_params(self) -> None: self.lane_change_bsm_delay = self.params.get_bool("AutoLaneChangeBsmDelay") - try: - self.lane_change_set_timer = int(self.params.get("AutoLaneChangeTimer", encoding="utf8")) - except (ValueError, TypeError): - self.lane_change_set_timer = AutoLaneChangeMode.NUDGE + self.lane_change_set_timer = self.params.get("AutoLaneChangeTimer", return_default=True) def update_params(self) -> None: if self.param_read_counter % 50 == 0: diff --git a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py index fb86280358..2f27e7e42f 100644 --- a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py +++ b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py @@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details. """ from cereal import car -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.params import Params @@ -21,7 +21,7 @@ class BlinkerPauseLateral: def get_params(self) -> None: self.enabled = self.params.get_bool("BlinkerPauseLateralControl") self.is_metric = self.params.get_bool("IsMetric") - self.min_speed = int(self.params.get("BlinkerMinLateralControlSpeed", encoding='utf8')) + self.min_speed = self.params.get("BlinkerMinLateralControlSpeed") def update(self, CS: car.CarState) -> bool: if not self.enabled: diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py index 1127619221..6a39b31723 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py @@ -10,8 +10,6 @@ import numpy as np from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from openpilot.selfdrive.modeld.constants import ModelConstants -from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle - LAT_PLAN_MIN_IDX = 5 LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan @@ -43,13 +41,12 @@ def get_lookahead_value(future_vals, current_val): return min_val -class LatControlTorqueExtBase(LagdToggle): +class LatControlTorqueExtBase: def __init__(self, lac_torque, CP, CP_SP): - LagdToggle.__init__(self) self.model_v2 = None self.model_valid = False self.torque_params = lac_torque.torque_params - self.use_steering_angle = lac_torque.torque_params.useSteeringAngle + self.use_steering_angle = True # FIXME-SP: deprecated in upstream self.actual_lateral_jerk: float = 0.0 self.lateral_jerk_setpoint: float = 0.0 diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py index 2f89594c8f..77d46ace34 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -8,7 +8,7 @@ from collections import deque import math import numpy as np -from opendbc.car.interfaces import LatControlInputs +from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.selfdrive.modeld.constants import ModelConstants @@ -127,7 +127,4 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): # apply friction override for cars with low NN friction response if self.model.friction_override: - self._pid_log.error += self.torque_from_lateral_accel(LatControlInputs(0.0, 0.0, CS.vEgo, CS.aEgo), self.torque_params, - friction_input, - self._lateral_accel_deadzone, friction_compensation=True, - gravity_adjusted=False) + self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) diff --git a/sunnypilot/selfdrive/controls/lib/param_store.py b/sunnypilot/selfdrive/controls/lib/param_store.py index 5ec2a1c8c9..fae221bb74 100644 --- a/sunnypilot/selfdrive/controls/lib/param_store.py +++ b/sunnypilot/selfdrive/controls/lib/param_store.py @@ -26,7 +26,7 @@ class ParamStore: def update(self, params: Params) -> None: old_values = dict(self.values) - self.values = {k: params.get(k, encoding='utf8') or "0" for k in self.keys} + self.values = {k: params.get(k) or "0" for k in self.keys} if old_values != self.values: self.cached_params_list = None diff --git a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py index 6ba1dcef41..ca8f16fe74 100644 --- a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py +++ b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py @@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details. """ from cereal import car -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral diff --git a/sunnypilot/sunnylink/api.py b/sunnypilot/sunnylink/api.py index eb9343af9a..b7838a40b0 100644 --- a/sunnypilot/sunnylink/api.py +++ b/sunnypilot/sunnylink/api.py @@ -46,8 +46,8 @@ class SunnylinkApi(BaseApi): time.sleep(0.5) def _resolve_dongle_ids(self): - sunnylink_dongle_id = self.params.get("SunnylinkDongleId", encoding='utf-8') - comma_dongle_id = self.dongle_id or self.params.get("DongleId", encoding='utf-8') + sunnylink_dongle_id = self.params.get("SunnylinkDongleId") + comma_dongle_id = self.dongle_id or self.params.get("DongleId") return sunnylink_dongle_id, comma_dongle_id def _resolve_imeis(self): @@ -63,7 +63,7 @@ class SunnylinkApi(BaseApi): return imei1, imei2 def _resolve_serial(self): - return (self.params.get("HardwareSerial", encoding='utf8') + return (self.params.get("HardwareSerial") or HARDWARE.get_serial()) def register_device(self, spinner=None, timeout=60, verbose=False): @@ -90,7 +90,7 @@ class SunnylinkApi(BaseApi): sunnylink_dongle_id = UNREGISTERED_SUNNYLINK_DONGLE_ID self._status_update("Public key not found, setting dongle ID to unregistered.") else: - Params().put("LastSunnylinkPingTime", "0") # Reset the last ping time to 0 if we are trying to register + Params().put("LastSunnylinkPingTime", 0) # Reset the last ping time to 0 if we are trying to register with pubkey_path.open() as f1, privkey_path.open() as f2: public_key = f1.read() private_key = f2.read() @@ -148,7 +148,7 @@ class SunnylinkApi(BaseApi): # Set the last ping time to the current time since we were just talking to the API last_ping = int(time.monotonic() * 1e9) if successful_registration else start_time - Params().put("LastSunnylinkPingTime", str(last_ping)) + Params().put("LastSunnylinkPingTime", last_ping) # Disable sunnylink if registration was not successful if not successful_registration: diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/sunnypilot/sunnylink/athena/sunnylinkd.py index a675820628..42b7d779e3 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -31,7 +31,7 @@ SUNNYLINK_RECONNECT_TIMEOUT_S = 70 # FYI changing this will also would require DISALLOW_LOG_UPLOAD = threading.Event() params = Params() -sunnylink_dongle_id = params.get("SunnylinkDongleId", encoding='utf-8') +sunnylink_dongle_id = params.get("SunnylinkDongleId") sunnylink_api = SunnylinkApi(sunnylink_dongle_id) @@ -68,7 +68,7 @@ def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None: end_event.set() comma_prime_cellular_end_event.set() - prime_type = params.get("PrimeType", encoding='utf-8') or 0 + prime_type = params.get("PrimeType") or 0 metered = sm['deviceState'].networkMetered if DISALLOW_LOG_UPLOAD.is_set() and not comma_prime_cellular_end_event.is_set(): @@ -103,7 +103,7 @@ def ws_recv(ws: WebSocket, end_event: threading.Event) -> None: elif opcode in (ABNF.OPCODE_PING, ABNF.OPCODE_PONG): cloudlog.debug("sunnylinkd.ws_recv.pong") last_ping = int(time.monotonic() * 1e9) - Params().put("LastSunnylinkPingTime", str(last_ping)) + Params().put("LastSunnylinkPingTime", last_ping) except WebSocketTimeoutException: ns_since_last_ping = int(time.monotonic() * 1e9) - last_ping if ns_since_last_ping > SUNNYLINK_RECONNECT_TIMEOUT_S * 1e9: diff --git a/sunnypilot/sunnylink/backups/manager.py b/sunnypilot/sunnylink/backups/manager.py index d6408d7ba7..f98088a1fb 100644 --- a/sunnypilot/sunnylink/backups/manager.py +++ b/sunnypilot/sunnylink/backups/manager.py @@ -12,7 +12,7 @@ from enum import Enum from typing import Any from openpilot.common.git import get_branch -from openpilot.common.params import Params, ParamKeyType +from openpilot.common.params import Params, ParamKeyType, ParamKeyFlag from openpilot.common.realtime import Ratekeeper from openpilot.common.swaglog import cloudlog from openpilot.system.version import get_version @@ -32,7 +32,7 @@ class BackupManagerSP: def __init__(self): self.params = Params() - self.device_id = self.params.get("SunnylinkDongleId", encoding="utf8") + self.device_id = self.params.get("SunnylinkDongleId") self.api = SunnylinkApi(self.device_id) self.pm = messaging.PubMaster(["backupManagerSP"]) @@ -72,9 +72,9 @@ class BackupManagerSP: def _collect_config_data(self) -> dict[str, Any]: """Collects configuration data to be backed up.""" config_data = {} - params_to_backup = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyType.BACKUP)] + params_to_backup = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyFlag.BACKUP)] for param in params_to_backup: - value = self.params.get(param) + value = str(self.params.get(param)).encode('utf-8') if value is not None: config_data[param] = base64.b64encode(value).decode('utf-8') return config_data @@ -185,25 +185,42 @@ class BackupManagerSP: def _apply_config(self, config_data: dict[str, str], all_values_encoded: bool = False) -> None: """Applies configuration data from a backup, but only for parameters marked as backupable.""" - # Get the current list of parameters that can be backed up - backupable_params = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyType.BACKUP)] + backupable_params = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyFlag.BACKUP)] + backupable_set_lower = {p.lower() for p in backupable_params} - # Count for logging/reporting restored_count = 0 skipped_count = 0 for param, encoded_value in config_data.items(): - try: - # Only restore parameters that are currently marked as backupable - if param in backupable_params: + if param.lower() in backupable_set_lower: + # Find real param name (with correct casing) + real_param = next(p for p in backupable_params if p.lower() == param.lower()) + param_type = self.params.get_type(real_param) + try: value = base64.b64decode(encoded_value) if all_values_encoded else encoded_value - self.params.put(param, value) + + if param_type != ParamKeyType.BYTES: + value = value.decode('utf-8') # type: ignore + + if param_type == ParamKeyType.STRING: + value = value + elif param_type == ParamKeyType.BOOL: + value = value.lower() in ('true', '1', 'yes') # type: ignore + elif param_type == ParamKeyType.INT: + value = int(value) # type: ignore + elif param_type == ParamKeyType.FLOAT: + value = float(value) # type: ignore + elif param_type == ParamKeyType.TIME: + value = str(value) + elif param_type == ParamKeyType.JSON: + value = json.loads(value) + self.params.put(real_param, value) restored_count += 1 - else: - skipped_count += 1 - cloudlog.info(f"Skipped restoring param {param}: not marked for backup in current version") - except Exception as e: - cloudlog.error(f"Failed to restore param {param}: {str(e)}") + except Exception as e: + cloudlog.error(f"Failed to restore param {param}: {str(e)}") + else: + skipped_count += 1 + cloudlog.info(f"Skipped restoring param {param}: not marked for backup in current version") cloudlog.info(f"Restore complete: {restored_count} params restored, {skipped_count} params skipped") @@ -253,7 +270,7 @@ class BackupManagerSP: self.params.remove("BackupManager_CreateBackup") # Check for restore command - restore_version = self.params.get("BackupManager_RestoreVersion", encoding="utf8") + restore_version = self.params.get("BackupManager_RestoreVersion") if restore_version: try: version = int(restore_version) if restore_version.isdigit() else None diff --git a/sunnypilot/sunnylink/uploader.py b/sunnypilot/sunnylink/uploader.py index 69ef2cfd8a..b9117f2866 100755 --- a/sunnypilot/sunnylink/uploader.py +++ b/sunnypilot/sunnylink/uploader.py @@ -92,7 +92,7 @@ class Uploader: self.immediate_priority = {"qlog": 0, "qlog.bz2": 0, "qcamera.ts": 1} def list_upload_files(self, metered: bool) -> Iterator[tuple[str, str, str]]: - r = self.params.get("AthenadRecentlyViewedRoutes", encoding="utf8") + r = self.params.get("AthenadRecentlyViewedRoutes") requested_routes = [] if r is None else r.split(",") for logdir in listdir_by_creation(self.root): @@ -250,7 +250,7 @@ def main(exit_event: threading.Event = None) -> None: clear_locks(Paths.log_root()) params = Params() - dongle_id = params.get("SunnylinkDongleId", encoding='utf8') + dongle_id = params.get("SunnylinkDongleId") offroad_transition_prev = 0. offroad_last = False diff --git a/sunnypilot/sunnylink/utils.py b/sunnypilot/sunnylink/utils.py index 55b1f6f285..35714eafe3 100644 --- a/sunnypilot/sunnylink/utils.py +++ b/sunnypilot/sunnylink/utils.py @@ -7,7 +7,7 @@ def get_sunnylink_status(params=None) -> tuple[bool, bool]: """Get the status of Sunnylink on the device. Returns a tuple of (is_sunnylink_enabled, is_registered).""" params = params or Params() is_sunnylink_enabled = params.get_bool("SunnylinkEnabled") - is_registered = params.get("SunnylinkDongleId", encoding='utf-8') not in (None, UNREGISTERED_SUNNYLINK_DONGLE_ID) + is_registered = params.get("SunnylinkDongleId") not in (None, UNREGISTERED_SUNNYLINK_DONGLE_ID) return is_sunnylink_enabled, is_registered @@ -51,7 +51,7 @@ def register_sunnylink(): def get_api_token(): """Get the API token for the device.""" params = Params() - sunnylink_dongle_id = params.get("SunnylinkDongleId", encoding='utf-8') + sunnylink_dongle_id = params.get("SunnylinkDongleId") sunnylink_api = SunnylinkApi(sunnylink_dongle_id) token = sunnylink_api.get_token() print(f"API Token: {token}") diff --git a/system/athena/athenad.py b/system/athena/athenad.py index 244f841e36..f97b8e55bb 100755 --- a/system/athena/athenad.py +++ b/system/athena/athenad.py @@ -153,7 +153,7 @@ class UploadQueueCache: try: upload_queue_json = Params().get("AthenadUploadQueue") if upload_queue_json is not None: - for item in json.loads(upload_queue_json): + for item in upload_queue_json: upload_queue.put(UploadItem.from_dict(item)) except Exception: cloudlog.exception("athena.UploadQueueCache.initialize.exception") @@ -163,7 +163,7 @@ class UploadQueueCache: try: queue: list[UploadItem | None] = list(upload_queue.queue) items = [asdict(i) for i in queue if i is not None and (i.id not in cancelled_uploads)] - Params().put("AthenadUploadQueue", json.dumps(items)) + Params().put("AthenadUploadQueue", items) except Exception: cloudlog.exception("athena.UploadQueueCache.cache.exception") @@ -441,7 +441,7 @@ def uploadFilesToUrls(files_data: list[UploadFileDict]) -> UploadFilesToUrlRespo path=path, url=file.url, headers=file.headers, - created_at=int(time.time() * 1000), + created_at=int(time.time() * 1000), # noqa: TID251 id=None, allow_cellular=file.allow_cellular, priority=file.priority, @@ -485,7 +485,7 @@ def setRouteViewed(route: str) -> dict[str, int | str]: # maintain a list of the last 10 routes viewed in connect params = Params() - r = params.get("AthenadRecentlyViewedRoutes", encoding="utf8") + r = params.get("AthenadRecentlyViewedRoutes") routes = [] if r is None else r.split(",") routes.append(route) @@ -498,7 +498,7 @@ def setRouteViewed(route: str) -> dict[str, int | str]: def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]: cloudlog.debug("athena.startLocalProxy.starting") - dongle_id = Params().get("DongleId").decode('utf8') + dongle_id = Params().get("DongleId") identity_token = Api(dongle_id).get_token() ws = create_connection(remote_ws_uri, cookie="jwt=" + identity_token, enable_multithread=True) @@ -551,12 +551,12 @@ def getPublicKey() -> str | None: @dispatcher.add_method def getSshAuthorizedKeys() -> str: - return Params().get("GithubSshKeys", encoding='utf8') or '' + return cast(str, Params().get("GithubSshKeys") or "") @dispatcher.add_method def getGithubUsername() -> str: - return Params().get("GithubUsername", encoding='utf8') or '' + return cast(str, Params().get("GithubUsername") or "") @dispatcher.add_method def getSimInfo(): @@ -599,7 +599,7 @@ def takeSnapshot() -> str | dict[str, str] | None: def get_logs_to_send_sorted(log_attr_name=LOG_ATTR_NAME) -> list[str]: # TODO: scan once then use inotify to detect file creation/deletion - curr_time = int(time.time()) + curr_time = int(time.time()) # noqa: TID251 logs = [] for log_entry in os.listdir(Paths.swaglog_root()): log_path = os.path.join(Paths.swaglog_root(), log_entry) @@ -697,7 +697,7 @@ def log_handler(end_event: threading.Event, log_attr_name=LOG_ATTR_NAME) -> None log_entry = log_files.pop() # newest log file cloudlog.debug(f"athena.log_handler.forward_request {log_entry}") try: - curr_time = int(time.time()) + curr_time = int(time.time()) # noqa: TID251 log_path = os.path.join(Paths.swaglog_root(), log_entry) setxattr(log_path, log_attr_name, int.to_bytes(curr_time, 4, sys.byteorder)) @@ -820,7 +820,7 @@ def ws_recv(ws: WebSocket, end_event: threading.Event) -> None: recv_queue.put_nowait(data) elif opcode == ABNF.OPCODE_PING: last_ping = int(time.monotonic() * 1e9) - Params().put("LastAthenaPingTime", str(last_ping)) + Params().put("LastAthenaPingTime", last_ping) except WebSocketTimeoutException: ns_since_last_ping = int(time.monotonic() * 1e9) - last_ping if ns_since_last_ping > RECONNECT_TIMEOUT_S * 1e9: @@ -885,7 +885,7 @@ def main(exit_event: threading.Event = None): cloudlog.exception("failed to set core affinity") params = Params() - dongle_id = params.get("DongleId", encoding='utf-8') + dongle_id = params.get("DongleId") UploadQueueCache.initialize(upload_queue) ws_uri = ATHENA_HOST + "/ws/v2/" + dongle_id diff --git a/system/athena/manage_athenad.py b/system/athena/manage_athenad.py index 7158cd9220..98557db06a 100755 --- a/system/athena/manage_athenad.py +++ b/system/athena/manage_athenad.py @@ -18,7 +18,7 @@ def main(): def manage_athenad(dongle_id_param, pid_param, process_name, target): params = Params() - dongle_id = params.get(dongle_id_param, encoding='utf-8') + dongle_id = params.get(dongle_id_param) build_metadata = get_build_metadata() cloudlog.bind_global(dongle_id=dongle_id, diff --git a/system/athena/registration.py b/system/athena/registration.py index fce984c03a..2c099c4bce 100755 --- a/system/athena/registration.py +++ b/system/athena/registration.py @@ -17,7 +17,7 @@ from openpilot.common.swaglog import cloudlog UNREGISTERED_DONGLE_ID = "UnregisteredDevice" def is_registered_device() -> bool: - dongle = Params().get("DongleId", encoding='utf-8') + dongle = Params().get("DongleId") return dongle not in (None, UNREGISTERED_DONGLE_ID) @@ -33,7 +33,7 @@ def register(show_spinner=False) -> str | None: """ params = Params() - dongle_id: str | None = params.get("DongleId", encoding='utf8') + dongle_id: str | None = params.get("DongleId") if dongle_id is None and Path(Paths.persist_root()+"/comma/dongle_id").is_file(): # not all devices will have this; added early in comma 3X production (2/28/24) with open(Paths.persist_root()+"/comma/dongle_id") as f: @@ -98,7 +98,7 @@ def register(show_spinner=False) -> str | None: if dongle_id: params.put("DongleId", dongle_id) - set_offroad_alert("Offroad_UnofficialHardware", (dongle_id == UNREGISTERED_DONGLE_ID) and not PC) + set_offroad_alert("Offroad_UnregisteredHardware", (dongle_id == UNREGISTERED_DONGLE_ID) and not PC) return dongle_id diff --git a/system/athena/tests/test_athenad.py b/system/athena/tests/test_athenad.py index e559e0c05c..99ac3b1c6b 100644 --- a/system/athena/tests/test_athenad.py +++ b/system/athena/tests/test_athenad.py @@ -66,9 +66,9 @@ class TestAthenadMethods: def setup_method(self): self.default_params = { "DongleId": "0000000000000000", - "GithubSshKeys": b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC307aE+nuHzTAgaJhzSf5v7ZZQW9gaperjhCmyPyl4PzY7T1mDGenTlVTN7yoVFZ9UfO9oMQqo0n1OwDIiqbIFxqnhrHU0cYfj88rI85m5BEKlNu5RdaVTj1tcbaPpQc5kZEolaI1nDDjzV0lwS7jo5VYDHseiJHlik3HH1SgtdtsuamGR2T80q1SyW+5rHoMOJG73IH2553NnWuikKiuikGHUYBd00K1ilVAK2xSiMWJp55tQfZ0ecr9QjEsJ+J/efL4HqGNXhffxvypCXvbUYAFSddOwXUPo5BTKevpxMtH+2YrkpSjocWA04VnTYFiPG6U4ItKmbLOTFZtPzoez private", # noqa: E501 - "GithubUsername": b"commaci", - "AthenadUploadQueue": '[]', + "GithubSshKeys": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC307aE+nuHzTAgaJhzSf5v7ZZQW9gaperjhCmyPyl4PzY7T1mDGenTlVTN7yoVFZ9UfO9oMQqo0n1OwDIiqbIFxqnhrHU0cYfj88rI85m5BEKlNu5RdaVTj1tcbaPpQc5kZEolaI1nDDjzV0lwS7jo5VYDHseiJHlik3HH1SgtdtsuamGR2T80q1SyW+5rHoMOJG73IH2553NnWuikKiuikGHUYBd00K1ilVAK2xSiMWJp55tQfZ0ecr9QjEsJ+J/efL4HqGNXhffxvypCXvbUYAFSddOwXUPo5BTKevpxMtH+2YrkpSjocWA04VnTYFiPG6U4ItKmbLOTFZtPzoez private", # noqa: E501 + "GithubUsername": "commaci", + "AthenadUploadQueue": [], } self.params = Params() @@ -91,8 +91,8 @@ class TestAthenadMethods: @staticmethod def _wait_for_upload(): - now = time.time() - while time.time() - now < 5: + now = time.monotonic() + while time.monotonic() - now < 5: if athenad.upload_queue.qsize() == 0: break @@ -190,11 +190,11 @@ class TestAthenadMethods: fn = self._create_file('qlog', data=os.urandom(10000 * 1024)) upload_fn = fn + ('.zst' if compress else '') - item = athenad.UploadItem(path=upload_fn, url="http://localhost:1238", headers={}, created_at=int(time.time()*1000), id='') + item = athenad.UploadItem(path=upload_fn, url="http://localhost:1238", headers={}, created_at=int(time.time()*1000), id='') # noqa: TID251 with pytest.raises(requests.exceptions.ConnectionError): athenad._do_upload(item) - item = athenad.UploadItem(path=upload_fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='') + item = athenad.UploadItem(path=upload_fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='') # noqa: TID251 resp = athenad._do_upload(item) assert resp.status_code == 201 @@ -226,7 +226,7 @@ class TestAthenadMethods: @with_upload_handler def test_upload_handler(self, host): fn = self._create_file('qlog.zst') - item = athenad.UploadItem(path=fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) + item = athenad.UploadItem(path=fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) # noqa: TID251 athenad.upload_queue.put_nowait(item) self._wait_for_upload() @@ -242,7 +242,7 @@ class TestAthenadMethods: mock_put = mocker.patch('openpilot.system.athena.athenad.UPLOAD_SESS.put') mock_put.return_value.__enter__.return_value.status_code = status fn = self._create_file('qlog.zst') - item = athenad.UploadItem(path=fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) + item = athenad.UploadItem(path=fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) # noqa: TID251 athenad.upload_queue.put_nowait(item) self._wait_for_upload() @@ -257,7 +257,7 @@ class TestAthenadMethods: def test_upload_handler_timeout(self): """When an upload times out or fails to connect it should be placed back in the queue""" fn = self._create_file('qlog.zst') - item = athenad.UploadItem(path=fn, url="http://localhost:44444/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) + item = athenad.UploadItem(path=fn, url="http://localhost:44444/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) # noqa: TID251 item_no_retry = replace(item, retry_count=MAX_RETRY_COUNT) athenad.upload_queue.put_nowait(item_no_retry) @@ -278,7 +278,7 @@ class TestAthenadMethods: @with_upload_handler def test_cancel_upload(self): item = athenad.UploadItem(path="qlog.zst", url="http://localhost:44444/qlog.zst", headers={}, - created_at=int(time.time()*1000), id='id', allow_cellular=True) + created_at=int(time.time()*1000), id='id', allow_cellular=True) # noqa: TID251 athenad.upload_queue.put_nowait(item) dispatcher["cancelUpload"](item.id) @@ -312,7 +312,7 @@ class TestAthenadMethods: @with_upload_handler def test_list_upload_queue_current(self, host: str): fn = self._create_file('qlog.zst') - item = athenad.UploadItem(path=fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) + item = athenad.UploadItem(path=fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) # noqa: TID251 athenad.upload_queue.put_nowait(item) self._wait_for_upload() @@ -331,7 +331,7 @@ class TestAthenadMethods: path=fp, url=f"http://localhost:44444/{fn}", headers={}, - created_at=int(time.time()*1000), + created_at=int(time.time()*1000), # noqa: TID251 id='', allow_cellular=True, priority=i @@ -343,7 +343,7 @@ class TestAthenadMethods: def test_list_upload_queue(self): item = athenad.UploadItem(path="qlog.zst", url="http://localhost:44444/qlog.zst", headers={}, - created_at=int(time.time()*1000), id='id', allow_cellular=True) + created_at=int(time.time()*1000), id='id', allow_cellular=True) # noqa: TID251 athenad.upload_queue.put_nowait(item) items = dispatcher["listUploadQueue"]() @@ -356,8 +356,8 @@ class TestAthenadMethods: assert len(items) == 0 def test_upload_queue_persistence(self): - item1 = athenad.UploadItem(path="_", url="_", headers={}, created_at=int(time.time()), id='id1') - item2 = athenad.UploadItem(path="_", url="_", headers={}, created_at=int(time.time()), id='id2') + item1 = athenad.UploadItem(path="_", url="_", headers={}, created_at=int(time.time()), id='id1') # noqa: TID251 + item2 = athenad.UploadItem(path="_", url="_", headers={}, created_at=int(time.time()), id='id2') # noqa: TID251 athenad.upload_queue.put_nowait(item1) athenad.upload_queue.put_nowait(item2) @@ -400,11 +400,11 @@ class TestAthenadMethods: def test_get_ssh_authorized_keys(self): keys = dispatcher["getSshAuthorizedKeys"]() - assert keys == self.default_params["GithubSshKeys"].decode('utf-8') + assert keys == self.default_params["GithubSshKeys"] def test_get_github_username(self): keys = dispatcher["getGithubUsername"]() - assert keys == self.default_params["GithubUsername"].decode('utf-8') + assert keys == self.default_params["GithubUsername"] def test_get_version(self): resp = dispatcher["getVersion"]() diff --git a/system/athena/tests/test_athenad_ping.py b/system/athena/tests/test_athenad_ping.py index 025df7d614..8ff1e37a5d 100644 --- a/system/athena/tests/test_athenad_ping.py +++ b/system/athena/tests/test_athenad_ping.py @@ -28,7 +28,7 @@ class TestAthenadPing: exit_event: threading.Event def _get_ping_time(self) -> str | None: - return cast(str | None, self.params.get("LastAthenaPingTime", encoding="utf-8")) + return cast(str | None, self.params.get("LastAthenaPingTime")) def _clear_ping_time(self) -> None: self.params.remove("LastAthenaPingTime") @@ -42,7 +42,7 @@ class TestAthenadPing: def setup_method(self) -> None: self.params = Params() - self.dongle_id = self.params.get("DongleId", encoding="utf-8") + self.dongle_id = self.params.get("DongleId") wifi_radio(True) self._clear_ping_time() diff --git a/system/athena/tests/test_registration.py b/system/athena/tests/test_registration.py index c1dabb78ec..c20722659a 100644 --- a/system/athena/tests/test_registration.py +++ b/system/athena/tests/test_registration.py @@ -49,7 +49,7 @@ class TestRegistration: dongle = register() assert m.call_count == 0 assert dongle == UNREGISTERED_DONGLE_ID - assert self.params.get("DongleId", encoding='utf-8') == dongle + assert self.params.get("DongleId") == dongle def test_missing_cache(self, mocker): # keys exist but no dongle id @@ -63,7 +63,7 @@ class TestRegistration: # call again, shouldn't hit the API this time assert register() == dongle assert m.call_count == 1 - assert self.params.get("DongleId", encoding='utf-8') == dongle + assert self.params.get("DongleId") == dongle def test_unregistered(self, mocker): # keys exist, but unregistered @@ -73,4 +73,4 @@ class TestRegistration: dongle = register() assert m.call_count == 1 assert dongle == UNREGISTERED_DONGLE_ID - assert self.params.get("DongleId", encoding='utf-8') == dongle + assert self.params.get("DongleId") == dongle diff --git a/system/camerad/test/test_camerad.py b/system/camerad/test/test_camerad.py index ab76985972..1f3f97b082 100644 --- a/system/camerad/test/test_camerad.py +++ b/system/camerad/test/test_camerad.py @@ -84,7 +84,10 @@ class TestCamerad: # logMonoTime > SOF assert np.all((ts[c]['t'] - ts[c]['timestampSof']/1e9) > 1e-7) - assert np.all((ts[c]['t'] - ts[c]['timestampEof']/1e9) > 1e-7) + + # logMonoTime > EOF, needs some tolerance since EOF is (SOF + readout time) but there is noise in the SOF timestamping (done via IRQ) + assert np.mean((ts[c]['t'] - ts[c]['timestampEof']/1e9) > 1e-7) > 0.7 # should be mostly logMonoTime > EOF + assert np.all((ts[c]['t'] - ts[c]['timestampEof']/1e9) > -0.10) # when EOF > logMonoTime, it should never be more than two frames def test_stress_test(self): os.environ['SPECTRA_ERROR_PROB'] = '0.008' diff --git a/system/camerad/test/test_exposure.py b/system/camerad/test/test_exposure.py index f431c03410..b853b0f2f2 100644 --- a/system/camerad/test/test_exposure.py +++ b/system/camerad/test/test_exposure.py @@ -33,8 +33,8 @@ class TestCamerad: @with_processes(['camerad']) def test_camera_operation(self): passed = 0 - start = time.time() - while time.time() - start < TEST_TIME and passed < REPEAT: + start = time.monotonic() + while time.monotonic() - start < TEST_TIME and passed < REPEAT: rpic, dpic = get_snapshots(frame="roadCameraState", front_frame="driverCameraState") wpic, _ = get_snapshots(frame="wideRoadCameraState") diff --git a/system/hardware/base.h b/system/hardware/base.h index baf0f3c3da..df9700a017 100644 --- a/system/hardware/base.h +++ b/system/hardware/base.h @@ -10,9 +10,6 @@ // no-op base hw class class HardwareNone { public: - static constexpr float MAX_VOLUME = 0.7; - static constexpr float MIN_VOLUME = 0.2; - static std::string get_os_version() { return ""; } static std::string get_name() { return ""; } static cereal::InitData::DeviceType get_device_type() { return cereal::InitData::DeviceType::UNKNOWN; } diff --git a/system/hardware/base.py b/system/hardware/base.py index 3506be5145..b457ea4e17 100644 --- a/system/hardware/base.py +++ b/system/hardware/base.py @@ -199,9 +199,6 @@ class HardwareBase(ABC): def get_modem_temperatures(self): pass - @abstractmethod - def get_nvme_temperatures(self): - pass @abstractmethod def initialize_hardware(self): diff --git a/system/hardware/hardwared.py b/system/hardware/hardwared.py index 223faeaafd..7f9d971fbf 100755 --- a/system/hardware/hardwared.py +++ b/system/hardware/hardwared.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import fcntl import os -import json import queue import struct import threading @@ -19,13 +18,14 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_HW from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert -from openpilot.system.hardware import HARDWARE, TICI, AGNOS +from openpilot.system.hardware import HARDWARE, TICI, AGNOS, PC from openpilot.system.loggerd.config import get_available_percent from openpilot.system.statsd import statlog from openpilot.common.swaglog import cloudlog from openpilot.system.hardware.power_monitoring import PowerMonitoring from openpilot.system.hardware.fan_controller import TiciFanController from openpilot.system.version import terms_version, training_version +from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID ThermalStatus = log.DeviceState.ThermalStatus NetworkType = log.DeviceState.NetworkType @@ -38,7 +38,7 @@ ONROAD_CYCLE_TIME = 1 # seconds to wait offroad after requesting an onroad cycl ThermalBand = namedtuple("ThermalBand", ['min_temp', 'max_temp']) HardwareState = namedtuple("HardwareState", ['network_type', 'network_info', 'network_strength', 'network_stats', - 'network_metered', 'nvme_temps', 'modem_temps']) + 'network_metered', 'modem_temps']) # List of thermal bands. We will stay within this region as long as we are within the bounds. # When exiting the bounds, we'll jump to the lower or higher band. Bands are ordered in the dict. @@ -141,7 +141,6 @@ def hw_state_thread(end_event, hw_queue): network_strength=HARDWARE.get_network_strength(network_type), network_stats={'wwanTx': tx, 'wwanRx': rx}, network_metered=HARDWARE.get_network_metered(network_type), - nvme_temps=HARDWARE.get_nvme_temperatures(), modem_temps=modem_temps, ) @@ -172,6 +171,7 @@ def hardware_thread(end_event, hw_queue) -> None: onroad_conditions: dict[str, bool] = { "ignition": False, "not_onroad_cycle": True, + "device_temp_good": True, } startup_conditions: dict[str, bool] = {} startup_conditions_prev: dict[str, bool] = {} @@ -188,7 +188,6 @@ def hardware_thread(end_event, hw_queue) -> None: network_metered=False, network_strength=NetworkStrength.unknown, network_stats={'wwanTx': -1, 'wwanRx': -1}, - nvme_temps=[], modem_temps=[], ) @@ -202,6 +201,10 @@ def hardware_thread(end_event, hw_queue) -> None: params = Params() power_monitor = PowerMonitoring() + uptime_offroad: float = params.get("UptimeOffroad", return_default=True) + uptime_onroad: float = params.get("UptimeOnroad", return_default=True) + last_uptime_ts: float = time.monotonic() + HARDWARE.initialize_hardware() thermal_config = HARDWARE.get_thermal_config() @@ -267,7 +270,6 @@ def hardware_thread(end_event, hw_queue) -> None: if last_hw_state.network_info is not None: msg.deviceState.networkInfo = last_hw_state.network_info - msg.deviceState.nvmeTempC = last_hw_state.nvme_temps msg.deviceState.modemTempC = last_hw_state.modem_temps msg.deviceState.screenBrightnessPercent = HARDWARE.get_screen_brightness() @@ -304,6 +306,7 @@ def hardware_thread(end_event, hw_queue) -> None: # **** starting logic **** startup_conditions["up_to_date"] = params.get("Offroad_ConnectivityNeeded") is None or params.get_bool("DisableUpdates") or params.get_bool("SnoozeUpdate") + startup_conditions["no_excessive_actuation"] = params.get("Offroad_ExcessiveActuation") is None startup_conditions["not_uninstalling"] = not params.get_bool("DoUninstall") startup_conditions["accepted_terms"] = params.get("HasAcceptedTerms") == terms_version @@ -330,21 +333,17 @@ def hardware_thread(end_event, hw_queue) -> None: show_alert = (not onroad_conditions["device_temp_good"] or not startup_conditions["device_temp_engageable"]) and onroad_conditions["ignition"] set_offroad_alert_if_changed("Offroad_TemperatureTooHigh", show_alert, extra_text=extra_text) + # *** registration check *** + if not PC: + # we enforce this for our software, but you are welcome + # to make a different decision in your software + startup_conditions["registered_device"] = PC or (params.get("DongleId") != UNREGISTERED_DONGLE_ID) + # TODO: this should move to TICI.initialize_hardware, but we currently can't import params there if TICI and HARDWARE.get_device_type() == "tici": if not os.path.isfile("/persist/comma/living-in-the-moment"): if not Path("/data/media").is_mount(): set_offroad_alert_if_changed("Offroad_StorageMissing", True) - else: - # check for bad NVMe - try: - with open("/sys/block/nvme0n1/device/model") as f: - model = f.read().strip() - if not model.startswith("Samsung SSD 980") and params.get("Offroad_BadNvme") is None: - set_offroad_alert_if_changed("Offroad_BadNvme", True) - cloudlog.event("Unsupported NVMe", model=model, error=True) - except Exception: - pass # Handle offroad/onroad transition should_start = all(onroad_conditions.values()) @@ -411,7 +410,7 @@ def hardware_thread(end_event, hw_queue) -> None: last_ping = params.get("LastAthenaPingTime") if last_ping is not None: - msg.deviceState.lastAthenaPingTime = int(last_ping) + msg.deviceState.lastAthenaPingTime = last_ping msg.deviceState.thermalStatus = thermal_status pm.send("deviceState", msg) @@ -429,8 +428,6 @@ def hardware_thread(end_event, hw_queue) -> None: statlog.gauge("memory_temperature", msg.deviceState.memoryTempC) for i, temp in enumerate(msg.deviceState.pmicTempC): statlog.gauge(f"pmic{i}_temperature", temp) - for i, temp in enumerate(last_hw_state.nvme_temps): - statlog.gauge(f"nvme_temperature{i}", temp) for i, temp in enumerate(last_hw_state.modem_temps): statlog.gauge(f"modem_temperature{i}", temp) statlog.gauge("fan_speed_percent_desired", msg.deviceState.fanSpeedPercentDesired) @@ -451,12 +448,23 @@ def hardware_thread(end_event, hw_queue) -> None: # save last one before going onroad if rising_edge_started: try: - params.put("LastOffroadStatusPacket", json.dumps(dat)) + params.put("LastOffroadStatusPacket", dat) except Exception: cloudlog.exception("failed to save offroad status") params.put_bool_nonblocking("NetworkMetered", msg.deviceState.networkMetered) + now_ts = time.monotonic() + if off_ts: + uptime_offroad += now_ts - max(last_uptime_ts, off_ts) + elif started_ts: + uptime_onroad += now_ts - max(last_uptime_ts, started_ts) + last_uptime_ts = now_ts + + if (count % int(60. / DT_HW)) == 0: + params.put("UptimeOffroad", uptime_offroad) + params.put("UptimeOnroad", uptime_onroad) + count += 1 should_start_prev = should_start diff --git a/system/hardware/pc/hardware.py b/system/hardware/pc/hardware.py index 9a80f10bed..6c11896390 100644 --- a/system/hardware/pc/hardware.py +++ b/system/hardware/pc/hardware.py @@ -70,8 +70,6 @@ class Pc(HardwareBase): def get_modem_temperatures(self): return [] - def get_nvme_temperatures(self): - return [] def initialize_hardware(self): pass diff --git a/system/hardware/power_monitoring.py b/system/hardware/power_monitoring.py index 952aebd9d7..d0a1e623a7 100644 --- a/system/hardware/power_monitoring.py +++ b/system/hardware/power_monitoring.py @@ -29,12 +29,10 @@ class PowerMonitoring: self.car_voltage_instant_mV = 12e3 # Last value of peripheralState voltage self.integration_lock = threading.Lock() - car_battery_capacity_uWh = self.params.get("CarBatteryCapacity") - if car_battery_capacity_uWh is None: - car_battery_capacity_uWh = 0 + car_battery_capacity_uWh = self.params.get("CarBatteryCapacity") or 0 # Reset capacity if it's low - self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), int(car_battery_capacity_uWh)) + self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), car_battery_capacity_uWh) # Calculation tick def calculate(self, voltage: int | None, ignition: bool): @@ -58,7 +56,7 @@ class PowerMonitoring: self.car_battery_capacity_uWh = max(self.car_battery_capacity_uWh, 0) self.car_battery_capacity_uWh = min(self.car_battery_capacity_uWh, CAR_BATTERY_CAPACITY_uWh) if now - self.last_save_time >= 10: - self.params.put_nonblocking("CarBatteryCapacity", str(int(self.car_battery_capacity_uWh))) + self.params.put_nonblocking("CarBatteryCapacity", int(self.car_battery_capacity_uWh)) self.last_save_time = now # First measurement, set integration time @@ -114,15 +112,14 @@ class PowerMonitoring: :return: True if the max time offroad has been exceeded, False otherwise """ try: - param = self.params.get("MaxTimeOffroad", encoding="utf8") - sp_max_time_val_s = int(param) * 60 if param is not None and int(param) >= 0 else MAX_TIME_OFFROAD_S + param = self.params.get("MaxTimeOffroad") + sp_max_time_val_s = param * 60 if param is not None and param >= 0 else MAX_TIME_OFFROAD_S except Exception: sp_max_time_val_s = MAX_TIME_OFFROAD_S - return sp_max_time_val_s > 0 and offroad_time >= sp_max_time_val_s + return 0 < sp_max_time_val_s <= offroad_time - -# See if we need to shutdown + # See if we need to shutdown def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: float | None, started_seen: bool): if offroad_timestamp is None: return False diff --git a/system/hardware/tests/test_power_monitoring.py b/system/hardware/tests/test_power_monitoring.py index 3eec13dc4c..451efcd735 100644 --- a/system/hardware/tests/test_power_monitoring.py +++ b/system/hardware/tests/test_power_monitoring.py @@ -206,18 +206,18 @@ class TestPowerMonitoring: (None, MAX_TIME_OFFROAD_S + 1, True), # exceeds 30h (1800+ mins) # Valid max time values (in minutes) - ("60", 59, False), # under limit - ("60", 120, True), # over limit - ("10", 8, False), # under limit - ("10", 11, True), # over limit + (60, 59, False), # under limit + (60, 120, True), # over limit + (10, 8, False), # under limit + (10, 11, True), # over limit # Edge case: max time is zero → no limit enforced - ("0", 0, False), - ("0", 400, False), + (0, 0, False), + (0, 400, False), # Invalid max time formats or negative values → fallback to 30 hours - ("invalid", 100, False), # should fallback to 30h - ("-1", MAX_TIME_OFFROAD_S + 1, True), # should fallback to 30h, and exceed it + (-100, 100, False), # should fallback to 30h + (-1, MAX_TIME_OFFROAD_S + 1, True), # should fallback to 30h, and exceed it ] ) def test_max_time_offroad_exceeded(self, max_time_offroad, offroad_time_min, expected_result): diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index ec2557422c..7d99793a59 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -1,25 +1,25 @@ [ { "name": "xbl", - "url": "https://commadist.azureedge.net/agnosupdate/xbl-6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1.img.xz", - "hash": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1", - "hash_raw": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1", + "url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz", + "hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", + "hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", "size": 3282256, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "003a17ab1be68a696f7efe4c9938e8be511d4aacfc2f3211fc896bdc1681d089" + "ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89" }, { "name": "xbl_config", - "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535.img.xz", - "hash": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535", - "hash_raw": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535", + "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz", + "hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", + "hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", "size": 98124, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "2a855dd636cc94718b64bea83a44d0a31741ecaa8f72a63613ff348ec7404091" + "ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f" }, { "name": "abl", @@ -56,28 +56,28 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef.img.xz", - "hash": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", - "hash_raw": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", - "size": 18479104, + "url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz", + "hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", + "hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", + "size": 18515968, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "8d7094d774faa4e801e36b403a31b53b913b31d086f4dc682d2f64710c557e8a" + "ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img.xz", - "hash": "cccd7073d067027396f2afd49874729757db0bbbc79853a0bf2938bd356fe164", - "hash_raw": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", + "url": "https://commadist.azureedge.net/agnosupdate/system-18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643.img.xz", + "hash": "49faee0e9b084abf0ea46f87722e3366bbd0435fb6b25cce189295c1ff368da1", + "hash_raw": "18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643", "size": 5368709120, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "c7707f16ce7d977748677cc354e250943b4ff6c21b9a19a492053d32397cf9ec", + "ondevice_hash": "db07761be0130e35a9d3ea6bec8df231260d3e767ae770850f18f10e14d0ab3f", "alt": { - "hash": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", - "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img", + "hash": "18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643", + "url": "https://commadist.azureedge.net/agnosupdate/system-18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643.img", "size": 5368709120 } } diff --git a/system/hardware/tici/all-partitions.json b/system/hardware/tici/all-partitions.json index d28b48128b..8648544991 100644 --- a/system/hardware/tici/all-partitions.json +++ b/system/hardware/tici/all-partitions.json @@ -97,14 +97,14 @@ }, { "name": "persist", - "url": "https://commadist.azureedge.net/agnosupdate/persist-9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d.img.xz", - "hash": "9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d", - "hash_raw": "9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d", - "size": 33554432, + "url": "https://commadist.azureedge.net/agnosupdate/persist-d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786.img.xz", + "hash": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786", + "hash_raw": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786", + "size": 4096, "sparse": false, "full_check": true, "has_ab": false, - "ondevice_hash": "9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d" + "ondevice_hash": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786" }, { "name": "systemrw", @@ -130,25 +130,25 @@ }, { "name": "xbl", - "url": "https://commadist.azureedge.net/agnosupdate/xbl-6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1.img.xz", - "hash": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1", - "hash_raw": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1", + "url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz", + "hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", + "hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", "size": 3282256, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "003a17ab1be68a696f7efe4c9938e8be511d4aacfc2f3211fc896bdc1681d089" + "ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89" }, { "name": "xbl_config", - "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535.img.xz", - "hash": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535", - "hash_raw": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535", + "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz", + "hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", + "hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", "size": 98124, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "2a855dd636cc94718b64bea83a44d0a31741ecaa8f72a63613ff348ec7404091" + "ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f" }, { "name": "abl", @@ -339,62 +339,62 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef.img.xz", - "hash": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", - "hash_raw": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", - "size": 18479104, + "url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz", + "hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", + "hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", + "size": 18515968, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "8d7094d774faa4e801e36b403a31b53b913b31d086f4dc682d2f64710c557e8a" + "ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img.xz", - "hash": "cccd7073d067027396f2afd49874729757db0bbbc79853a0bf2938bd356fe164", - "hash_raw": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", + "url": "https://commadist.azureedge.net/agnosupdate/system-18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643.img.xz", + "hash": "49faee0e9b084abf0ea46f87722e3366bbd0435fb6b25cce189295c1ff368da1", + "hash_raw": "18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643", "size": 5368709120, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "c7707f16ce7d977748677cc354e250943b4ff6c21b9a19a492053d32397cf9ec", + "ondevice_hash": "db07761be0130e35a9d3ea6bec8df231260d3e767ae770850f18f10e14d0ab3f", "alt": { - "hash": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", - "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img", + "hash": "18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643", + "url": "https://commadist.azureedge.net/agnosupdate/system-18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643.img", "size": 5368709120 } }, { "name": "userdata_90", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_90-f0c675e0fae420870c9ba8979fa246b170f4f1a7a04b49609b55b6bdfa8c1b21.img.xz", - "hash": "3d8a007bae088c5959eb9b82454013f91868946d78380fecea2b1afdfb575c02", - "hash_raw": "f0c675e0fae420870c9ba8979fa246b170f4f1a7a04b49609b55b6bdfa8c1b21", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_90-02f7abb4b667c04043c0c6950145aaebd704851261f32256d0f7e84a52059dda.img.xz", + "hash": "1eda66d4e31222fc2e792a62ae8e7d322fc643f0b23785e7527bb51a9fee97c7", + "hash_raw": "02f7abb4b667c04043c0c6950145aaebd704851261f32256d0f7e84a52059dda", "size": 96636764160, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "5bfbabb8ff96b149056aa75d5b7e66a7cdd9cb4bcefe23b922c292f7f3a43462" + "ondevice_hash": "679b650ee04b7b1ef610b63fde9b43569fded39ceacf88789b564de99c221ea1" }, { "name": "userdata_89", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_89-06fc52be37b42690ed7b4f8c66c4611309a2dea9fca37dd9d27d1eff302eb1bf.img.xz", - "hash": "443f136484294b210318842d09fb618d5411c8bdbab9f7421d8c89eb291a8d3f", - "hash_raw": "06fc52be37b42690ed7b4f8c66c4611309a2dea9fca37dd9d27d1eff302eb1bf", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_89-bab8399bbe3968f3c496f7bc83c2541b33acc1f47814c4ad95801bf5cb7e7588.img.xz", + "hash": "e63d3277285aae1f04fd7f4f48429ce35010f4843ab755f10d360c3aa788e484", + "hash_raw": "bab8399bbe3968f3c496f7bc83c2541b33acc1f47814c4ad95801bf5cb7e7588", "size": 95563022336, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "67db02b29a7e4435951c64cc962a474d048ed444aa912f3494391417cd51a074" + "ondevice_hash": "2947374fc5980ffe3c5b94b61cc1c81bc55214f494153ed234164801731f5dc0" }, { "name": "userdata_30", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_30-06679488f0c5c3fcfd5f351133050751cd189f705e478a979c45fc4a166d18a6.img.xz", - "hash": "875b580cb786f290a842e9187fd945657561886123eb3075a26f7995a18068f6", - "hash_raw": "06679488f0c5c3fcfd5f351133050751cd189f705e478a979c45fc4a166d18a6", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_30-22c874b4b66bbc000f3219abede8d62cb307f5786fd526a8473c61422765dea0.img.xz", + "hash": "12d9245711e8c49c51ff2c7b82d7301f2fcb1911edcddb35a105a80911859113", + "hash_raw": "22c874b4b66bbc000f3219abede8d62cb307f5786fd526a8473c61422765dea0", "size": 32212254720, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "16e27ba3c5cf9f0394ce6235ba6021b8a2de293fdb08399f8ca832fa5e4d0b9d" + "ondevice_hash": "03c8b65c945207f887ed6c52d38b53d53d71c8597dcb0b63dfbb11f7cfff8d2b" } ] \ No newline at end of file diff --git a/system/hardware/tici/hardware.h b/system/hardware/tici/hardware.h index 179ef54a9b..ed8a7e7d17 100644 --- a/system/hardware/tici/hardware.h +++ b/system/hardware/tici/hardware.h @@ -13,8 +13,6 @@ class HardwareTici : public HardwareNone { public: - static constexpr float MAX_VOLUME = 0.9; - static constexpr float MIN_VOLUME = 0.1; static bool TICI() { return true; } static bool AGNOS() { return true; } static std::string get_os_version() { @@ -75,7 +73,7 @@ public: return; } - int value = util::map_val(std::clamp(percent, 0, 100), 0, 100, 0, 255); + int value = util::map_val(std::clamp(percent, 0, 100), 0, 100, 0, 300); std::ofstream("/sys/class/leds/led:switch_2/brightness") << 0 << "\n"; std::ofstream("/sys/class/leds/led:torch_2/brightness") << value << "\n"; std::ofstream("/sys/class/leds/led:switch_2/brightness") << value << "\n"; diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 20a23bd67e..35f9916c31 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -1,4 +1,3 @@ -import json import math import os import subprocess @@ -291,15 +290,6 @@ class Tici(HardwareBase): except Exception: return [] - def get_nvme_temperatures(self): - ret = [] - try: - out = subprocess.check_output("sudo smartctl -aj /dev/nvme0", shell=True) - dat = json.loads(out) - ret = list(map(int, dat["nvme_smart_health_information_log"]["temperature_sensors"])) - except Exception: - pass - return ret def get_current_power_draw(self): return (self.read_param_file("/sys/class/hwmon/hwmon1/power1_input", int) / 1e6) diff --git a/system/loggerd/bootlog.cc b/system/loggerd/bootlog.cc index a7b1ae8244..1b87ce394f 100644 --- a/system/loggerd/bootlog.cc +++ b/system/loggerd/bootlog.cc @@ -31,9 +31,6 @@ static kj::Array build_boot_log() { "[ -x \"$(command -v journalctl)\" ] && journalctl -o short-monotonic", }; - if (Hardware::TICI()) { - bootlog_commands.push_back("[ -e /dev/nvme0 ] && sudo nvme smart-log --output-format=json /dev/nvme0"); - } auto commands = boot.initCommands().initEntries(bootlog_commands.size()); for (int j = 0; j < bootlog_commands.size(); j++) { diff --git a/system/loggerd/encoder/encoder.cc b/system/loggerd/encoder/encoder.cc index b8f2f274d2..06922b0cd0 100644 --- a/system/loggerd/encoder/encoder.cc +++ b/system/loggerd/encoder/encoder.cc @@ -21,7 +21,7 @@ void VideoEncoder::publisher_publish(int segment_num, uint32_t idx, VisionIpcBuf edata.setFrameId(extra.frame_id); edata.setTimestampSof(extra.timestamp_sof); edata.setTimestampEof(extra.timestamp_eof); - edata.setType(encoder_info.encode_type); + edata.setType(encoder_info.get_settings(in_width).encode_type); edata.setEncodeId(cnt++); edata.setSegmentNum(segment_num); edata.setSegmentId(idx); diff --git a/system/loggerd/encoder/ffmpeg_encoder.cc b/system/loggerd/encoder/ffmpeg_encoder.cc index 4e6946363f..4d6be47182 100644 --- a/system/loggerd/encoder/ffmpeg_encoder.cc +++ b/system/loggerd/encoder/ffmpeg_encoder.cc @@ -1,5 +1,3 @@ -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - #include "system/loggerd/encoder/ffmpeg_encoder.h" #include @@ -48,7 +46,7 @@ FfmpegEncoder::~FfmpegEncoder() { } void FfmpegEncoder::encoder_open() { - auto codec_id = encoder_info.encode_type == cereal::EncodeIndex::Type::QCAMERA_H264 + auto codec_id = encoder_info.get_settings(in_width).encode_type == cereal::EncodeIndex::Type::QCAMERA_H264 ? AV_CODEC_ID_H264 : AV_CODEC_ID_FFVHUFF; const AVCodec *codec = avcodec_find_encoder(codec_id); @@ -119,8 +117,7 @@ int FfmpegEncoder::encode_frame(VisionBuf* buf, VisionIpcBufExtra *extra) { ret = -1; } - AVPacket pkt; - av_init_packet(&pkt); + AVPacket pkt = {}; pkt.data = NULL; pkt.size = 0; while (ret >= 0) { diff --git a/system/loggerd/encoder/v4l_encoder.cc b/system/loggerd/encoder/v4l_encoder.cc index ac1da09693..6ee3af13b0 100644 --- a/system/loggerd/encoder/v4l_encoder.cc +++ b/system/loggerd/encoder/v4l_encoder.cc @@ -24,14 +24,6 @@ */ const int env_debug_encoder = (getenv("DEBUG_ENCODER") != NULL) ? atoi(getenv("DEBUG_ENCODER")) : 0; -static void checked_ioctl(int fd, unsigned long request, void *argp) { - int ret = util::safe_ioctl(fd, request, argp); - if (ret != 0) { - LOGE("checked_ioctl failed with error %d (%d %lx %p)", errno, fd, request, argp); - assert(0); - } -} - static void dequeue_buffer(int fd, v4l2_buf_type buf_type, unsigned int *index=NULL, unsigned int *bytesused=NULL, unsigned int *flags=NULL, struct timeval *timestamp=NULL) { v4l2_plane plane = {0}; v4l2_buffer v4l_buf = { @@ -40,7 +32,7 @@ static void dequeue_buffer(int fd, v4l2_buf_type buf_type, unsigned int *index=N .m = { .planes = &plane, }, .length = 1, }; - checked_ioctl(fd, VIDIOC_DQBUF, &v4l_buf); + util::safe_ioctl(fd, VIDIOC_DQBUF, &v4l_buf, "VIDIOC_DQBUF failed"); if (index) *index = v4l_buf.index; if (bytesused) *bytesused = v4l_buf.m.planes[0].bytesused; @@ -66,8 +58,7 @@ static void queue_buffer(int fd, v4l2_buf_type buf_type, unsigned int index, Vis .flags = V4L2_BUF_FLAG_TIMESTAMP_COPY, .timestamp = timestamp }; - - checked_ioctl(fd, VIDIOC_QBUF, &v4l_buf); + util::safe_ioctl(fd, VIDIOC_QBUF, &v4l_buf, "VIDIOC_QBUF failed"); } static void request_buffers(int fd, v4l2_buf_type buf_type, unsigned int count) { @@ -76,7 +67,7 @@ static void request_buffers(int fd, v4l2_buf_type buf_type, unsigned int count) .memory = V4L2_MEMORY_USERPTR, .count = count }; - checked_ioctl(fd, VIDIOC_REQBUFS, &reqbuf); + util::safe_ioctl(fd, VIDIOC_REQBUFS, &reqbuf, "VIDIOC_REQBUFS failed"); } void V4LEncoder::dequeue_handler(V4LEncoder *e) { @@ -159,11 +150,14 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei assert(fd >= 0); struct v4l2_capability cap; - checked_ioctl(fd, VIDIOC_QUERYCAP, &cap); + util::safe_ioctl(fd, VIDIOC_QUERYCAP, &cap, "VIDIOC_QUERYCAP failed"); LOGD("opened encoder device %s %s = %d", cap.driver, cap.card, fd); assert(strcmp((const char *)cap.driver, "msm_vidc_driver") == 0); assert(strcmp((const char *)cap.card, "msm_vidc_venc") == 0); + EncoderSettings encoder_settings = encoder_info.get_settings(in_width); + bool is_h265 = encoder_settings.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C; + struct v4l2_format fmt_out = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, .fmt = { @@ -171,13 +165,13 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei // downscales are free with v4l .width = (unsigned int)(out_width), .height = (unsigned int)(out_height), - .pixelformat = (encoder_info.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C) ? V4L2_PIX_FMT_HEVC : V4L2_PIX_FMT_H264, + .pixelformat = is_h265 ? V4L2_PIX_FMT_HEVC : V4L2_PIX_FMT_H264, .field = V4L2_FIELD_ANY, .colorspace = V4L2_COLORSPACE_DEFAULT, } } }; - checked_ioctl(fd, VIDIOC_S_FMT, &fmt_out); + util::safe_ioctl(fd, VIDIOC_S_FMT, &fmt_out, "VIDIOC_S_FMT failed"); v4l2_streamparm streamparm = { .type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, @@ -191,7 +185,7 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei } } }; - checked_ioctl(fd, VIDIOC_S_PARM, &streamparm); + util::safe_ioctl(fd, VIDIOC_S_PARM, &streamparm, "VIDIOC_S_PARM failed"); struct v4l2_format fmt_in = { .type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, @@ -205,7 +199,7 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei } } }; - checked_ioctl(fd, VIDIOC_S_FMT, &fmt_in); + util::safe_ioctl(fd, VIDIOC_S_FMT, &fmt_in, "VIDIOC_S_FMT failed"); LOGD("in buffer size %d, out buffer size %d", fmt_in.fmt.pix_mp.plane_fmt[0].sizeimage, @@ -214,34 +208,32 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei // shared ctrls { struct v4l2_control ctrls[] = { + { .id = V4L2_CID_MPEG_VIDEO_BITRATE, .value = encoder_settings.bitrate}, + { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_P_FRAMES, .value = encoder_settings.gop_size - encoder_settings.b_frames - 1}, + { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_B_FRAMES, .value = encoder_settings.b_frames}, { .id = V4L2_CID_MPEG_VIDEO_HEADER_MODE, .value = V4L2_MPEG_VIDEO_HEADER_MODE_SEPARATE}, - { .id = V4L2_CID_MPEG_VIDEO_BITRATE, .value = encoder_info.bitrate}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_RATE_CONTROL, .value = V4L2_CID_MPEG_VIDC_VIDEO_RATE_CONTROL_VBR_CFR}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_PRIORITY, .value = V4L2_MPEG_VIDC_VIDEO_PRIORITY_REALTIME_DISABLE}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_IDR_PERIOD, .value = 1}, }; for (auto ctrl : ctrls) { - checked_ioctl(fd, VIDIOC_S_CTRL, &ctrl); + util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl, "VIDIOC_S_CTRL failed"); } } - if (encoder_info.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C) { + if (is_h265) { struct v4l2_control ctrls[] = { { .id = V4L2_CID_MPEG_VIDC_VIDEO_HEVC_PROFILE, .value = V4L2_MPEG_VIDC_VIDEO_HEVC_PROFILE_MAIN}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_HEVC_TIER_LEVEL, .value = V4L2_MPEG_VIDC_VIDEO_HEVC_LEVEL_HIGH_TIER_LEVEL_5}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_VUI_TIMING_INFO, .value = V4L2_MPEG_VIDC_VIDEO_VUI_TIMING_INFO_ENABLED}, - { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_P_FRAMES, .value = 29}, - { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_B_FRAMES, .value = 0}, }; for (auto ctrl : ctrls) { - checked_ioctl(fd, VIDIOC_S_CTRL, &ctrl); + util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl, "VIDIOC_S_CTRL failed"); } } else { struct v4l2_control ctrls[] = { { .id = V4L2_CID_MPEG_VIDEO_H264_PROFILE, .value = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH}, { .id = V4L2_CID_MPEG_VIDEO_H264_LEVEL, .value = V4L2_MPEG_VIDEO_H264_LEVEL_UNKNOWN}, - { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_P_FRAMES, .value = 14}, - { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_B_FRAMES, .value = 0}, { .id = V4L2_CID_MPEG_VIDEO_H264_ENTROPY_MODE, .value = V4L2_MPEG_VIDEO_H264_ENTROPY_MODE_CABAC}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL, .value = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL_0}, { .id = V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_MODE, .value = 0}, @@ -250,7 +242,7 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei { .id = V4L2_CID_MPEG_VIDEO_MULTI_SLICE_MODE, .value = 0}, }; for (auto ctrl : ctrls) { - checked_ioctl(fd, VIDIOC_S_CTRL, &ctrl); + util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl, "VIDIOC_S_CTRL failed"); } } @@ -260,9 +252,9 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei // start encoder v4l2_buf_type buf_type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; - checked_ioctl(fd, VIDIOC_STREAMON, &buf_type); + util::safe_ioctl(fd, VIDIOC_STREAMON, &buf_type, "VIDIOC_STREAMON failed"); buf_type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; - checked_ioctl(fd, VIDIOC_STREAMON, &buf_type); + util::safe_ioctl(fd, VIDIOC_STREAMON, &buf_type, "VIDIOC_STREAMON failed"); // queue up output buffers for (unsigned int i = 0; i < BUF_OUT_COUNT; i++) { @@ -305,7 +297,7 @@ void V4LEncoder::encoder_close() { for (int i = 0; i < BUF_IN_COUNT; i++) free_buf_in.push(i); // no frames, stop the encoder struct v4l2_encoder_cmd encoder_cmd = { .cmd = V4L2_ENC_CMD_STOP }; - checked_ioctl(fd, VIDIOC_ENCODER_CMD, &encoder_cmd); + util::safe_ioctl(fd, VIDIOC_ENCODER_CMD, &encoder_cmd, "VIDIOC_ENCODER_CMD failed"); // join waits for V4L2_QCOM_BUF_FLAG_EOS dequeue_handler_thread.join(); assert(extras.empty()); @@ -316,10 +308,10 @@ void V4LEncoder::encoder_close() { V4LEncoder::~V4LEncoder() { encoder_close(); v4l2_buf_type buf_type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; - checked_ioctl(fd, VIDIOC_STREAMOFF, &buf_type); + util::safe_ioctl(fd, VIDIOC_STREAMOFF, &buf_type, "VIDIOC_STREAMOFF failed"); request_buffers(fd, V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, 0); buf_type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; - checked_ioctl(fd, VIDIOC_STREAMOFF, &buf_type); + util::safe_ioctl(fd, VIDIOC_STREAMOFF, &buf_type, "VIDIOC_STREAMOFF failed"); request_buffers(fd, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, 0); close(fd); diff --git a/system/loggerd/logger.cc b/system/loggerd/logger.cc index f07aee1596..0ebe323939 100644 --- a/system/loggerd/logger.cc +++ b/system/loggerd/logger.cc @@ -59,7 +59,7 @@ kj::Array logger_build_init_data() { for (auto& [key, value] : params_map) { auto lentry = lparams[j]; lentry.setKey(key); - if ( !(params.getKeyType(key) & DONT_LOG) ) { + if ( !(params.getKeyFlag(key) & DONT_LOG) ) { lentry.setValue(capnp::Data::Reader((const kj::byte*)value.data(), value.size())); } j++; diff --git a/system/loggerd/loggerd.cc b/system/loggerd/loggerd.cc index 144ab9f349..21de1ff33f 100644 --- a/system/loggerd/loggerd.cc +++ b/system/loggerd/loggerd.cc @@ -194,7 +194,7 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct return bytes_count; } -void handle_user_flag(LoggerdState *s) { +void handle_preserve_segment(LoggerdState *s) { static int prev_segment = -1; if (s->logger.segment() == prev_segment) return; @@ -222,7 +222,7 @@ void loggerd_thread() { typedef struct ServiceState { std::string name; int counter, freq; - bool encoder, user_flag, record_audio; + bool encoder, preserve_segment, record_audio; } ServiceState; std::unordered_map service_state; std::unordered_map remote_encoders; @@ -246,7 +246,7 @@ void loggerd_thread() { .counter = 0, .freq = it.decimation, .encoder = encoder, - .user_flag = it.name == "userFlag", + .preserve_segment = (it.name == "userBookmark") || (it.name == "audioFeedback"), .record_audio = record_audio, }; } @@ -281,8 +281,8 @@ void loggerd_thread() { if (do_exit) break; ServiceState &service = service_state[sock]; - if (service.user_flag) { - handle_user_flag(&s); + if (service.preserve_segment) { + handle_preserve_segment(&s); } // drain socket diff --git a/system/loggerd/loggerd.h b/system/loggerd/loggerd.h index 5dfb178fd5..967caec867 100644 --- a/system/loggerd/loggerd.h +++ b/system/loggerd/loggerd.h @@ -13,10 +13,7 @@ #include "system/loggerd/logger.h" constexpr int MAIN_FPS = 20; -const int MAIN_BITRATE = 1e7; -const int LIVESTREAM_BITRATE = 1e6; -const int QCAM_BITRATE = 256000; - +const auto MAIN_ENCODE_TYPE = Hardware::PC() ? cereal::EncodeIndex::Type::BIG_BOX_LOSSLESS : cereal::EncodeIndex::Type::FULL_H_E_V_C; #define NO_CAMERA_PATIENCE 500 // fall back to time-based rotation if all cameras are dead #define INIT_ENCODE_FUNCTIONS(encode_type) \ @@ -29,6 +26,30 @@ const int SEGMENT_LENGTH = LOGGERD_TEST ? atoi(getenv("LOGGERD_SEGMENT_LENGTH")) constexpr char PRESERVE_ATTR_NAME[] = "user.preserve"; constexpr char PRESERVE_ATTR_VALUE = '1'; + +struct EncoderSettings { + cereal::EncodeIndex::Type encode_type; + int bitrate; + int gop_size; + int b_frames = 0; // we don't use b frames + + static EncoderSettings MainEncoderSettings(int in_width) { + if (in_width <= 1344) { + return EncoderSettings{.encode_type = MAIN_ENCODE_TYPE, .bitrate = 5'000'000, .gop_size = 20}; + } else { + return EncoderSettings{.encode_type = MAIN_ENCODE_TYPE, .bitrate = 10'000'000, .gop_size = 30}; + } + } + + static EncoderSettings QcamEncoderSettings() { + return EncoderSettings{.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .bitrate = 256'000, .gop_size = 15}; + } + + static EncoderSettings StreamEncoderSettings() { + return EncoderSettings{.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .bitrate = 1'000'000, .gop_size = 15}; + } +}; + class EncoderInfo { public: const char *publish_name; @@ -39,9 +60,8 @@ public: int frame_width = -1; int frame_height = -1; int fps = MAIN_FPS; - int bitrate = MAIN_BITRATE; - cereal::EncodeIndex::Type encode_type = Hardware::PC() ? cereal::EncodeIndex::Type::BIG_BOX_LOSSLESS - : cereal::EncodeIndex::Type::FULL_H_E_V_C; + std::function get_settings; + ::cereal::EncodeData::Reader (cereal::Event::Reader::*get_encode_data_func)() const; void (cereal::Event::Builder::*set_encode_idx_func)(::cereal::EncodeIndex::Reader); cereal::EncodeData::Builder (cereal::Event::Builder::*init_encode_data_func)(); @@ -59,12 +79,14 @@ const EncoderInfo main_road_encoder_info = { .publish_name = "roadEncodeData", .thumbnail_name = "thumbnail", .filename = "fcamera.hevc", + .get_settings = [](int in_width){return EncoderSettings::MainEncoderSettings(in_width);}, INIT_ENCODE_FUNCTIONS(RoadEncode), }; const EncoderInfo main_wide_road_encoder_info = { .publish_name = "wideRoadEncodeData", .filename = "ecamera.hevc", + .get_settings = [](int in_width){return EncoderSettings::MainEncoderSettings(in_width);}, INIT_ENCODE_FUNCTIONS(WideRoadEncode), }; @@ -72,39 +94,36 @@ const EncoderInfo main_driver_encoder_info = { .publish_name = "driverEncodeData", .filename = "dcamera.hevc", .record = Params().getBool("RecordFront"), + .get_settings = [](int in_width){return EncoderSettings::MainEncoderSettings(in_width);}, INIT_ENCODE_FUNCTIONS(DriverEncode), }; const EncoderInfo stream_road_encoder_info = { .publish_name = "livestreamRoadEncodeData", //.thumbnail_name = "thumbnail", - .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .record = false, - .bitrate = LIVESTREAM_BITRATE, + .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamRoadEncode), }; const EncoderInfo stream_wide_road_encoder_info = { .publish_name = "livestreamWideRoadEncodeData", - .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .record = false, - .bitrate = LIVESTREAM_BITRATE, + .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamWideRoadEncode), }; const EncoderInfo stream_driver_encoder_info = { .publish_name = "livestreamDriverEncodeData", - .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .record = false, - .bitrate = LIVESTREAM_BITRATE, + .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamDriverEncode), }; const EncoderInfo qcam_encoder_info = { .publish_name = "qRoadEncodeData", .filename = "qcamera.ts", - .bitrate = QCAM_BITRATE, - .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, + .get_settings = [](int){return EncoderSettings::QcamEncoderSettings();}, .frame_width = 526, .frame_height = 330, .include_audio = Params().getBool("RecordAudio"), diff --git a/system/loggerd/tests/loggerd_tests_common.py b/system/loggerd/tests/loggerd_tests_common.py index 15fd997eb8..87c3da65c2 100644 --- a/system/loggerd/tests/loggerd_tests_common.py +++ b/system/loggerd/tests/loggerd_tests_common.py @@ -76,7 +76,7 @@ class UploaderTestCase: self.seg_dir = self.seg_format.format(self.seg_num) self.params = Params() - self.params.put("IsOffroad", "1") + self.params.put("IsOffroad", True) self.params.put("DongleId", "0000000000000000") def make_file_with_data(self, f_dir: str, fn: str, size_mb: float = .1, lock: bool = False, diff --git a/system/loggerd/tests/test_encoder.py b/system/loggerd/tests/test_encoder.py index b24bfbe168..e4dabd3df9 100644 --- a/system/loggerd/tests/test_encoder.py +++ b/system/loggerd/tests/test_encoder.py @@ -19,11 +19,12 @@ from openpilot.system.hardware.hw import Paths SEGMENT_LENGTH = 2 FULL_SIZE = 2507572 +def hevc_size(w): return FULL_SIZE // 2 if w <= 1344 else FULL_SIZE CAMERAS = [ - ("fcamera.hevc", 20, FULL_SIZE, "roadEncodeIdx"), - ("dcamera.hevc", 20, FULL_SIZE, "driverEncodeIdx"), - ("ecamera.hevc", 20, FULL_SIZE, "wideRoadEncodeIdx"), - ("qcamera.ts", 20, 130000, None), + ("fcamera.hevc", 20, hevc_size, "roadEncodeIdx"), + ("dcamera.hevc", 20, hevc_size, "driverEncodeIdx"), + ("ecamera.hevc", 20, hevc_size, "wideRoadEncodeIdx"), + ("qcamera.ts", 20, lambda x: 130000, None), ] # we check frame count, so we don't have to be too strict on size @@ -76,7 +77,7 @@ class TestEncoder: # check each camera file size counts = [] first_frames = [] - for camera, fps, size, encode_idx_name in CAMERAS: + for camera, fps, size_lambda, encode_idx_name in CAMERAS: if not record_front and "dcamera" in camera: continue @@ -86,14 +87,14 @@ class TestEncoder: assert os.path.exists(file_path), f"segment #{i}: '{file_path}' missing" # TODO: this ffprobe call is really slow - # check frame count - cmd = f"ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets -of csv=p=0 {file_path}" + # get width and check frame count + cmd = f"ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets,width -of csv=p=0 {file_path}" if TICI: cmd = "LD_LIBRARY_PATH=/usr/local/lib " + cmd expected_frames = fps * SEGMENT_LENGTH - probe = subprocess.check_output(cmd, shell=True, encoding='utf8') - frame_count = int(probe.split('\n')[0].strip()) + probe = subprocess.check_output(cmd, shell=True, encoding='utf8').split('\n')[0].strip().split(',') + frame_width, frame_count = int(probe[0]), int(probe[1]) counts.append(frame_count) assert frame_count == expected_frames, \ @@ -101,8 +102,9 @@ class TestEncoder: # sanity check file size file_size = os.path.getsize(file_path) - assert math.isclose(file_size, size, rel_tol=FILE_SIZE_TOLERANCE), \ - f"{file_path} size {file_size} isn't close to target size {size}" + target_size = size_lambda(frame_width) + assert math.isclose(file_size, target_size, rel_tol=FILE_SIZE_TOLERANCE), \ + f"{file_path} size {file_size} isn't close to target size {target_size}" # Check encodeIdx if encode_idx_name is not None: diff --git a/system/loggerd/tests/test_loggerd.py b/system/loggerd/tests/test_loggerd.py index 8e2f52a731..c6a4b12e63 100644 --- a/system/loggerd/tests/test_loggerd.py +++ b/system/loggerd/tests/test_loggerd.py @@ -182,7 +182,7 @@ class TestLoggerd: @pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing def test_rotation(self): - Params().put("RecordFront", "1") + Params().put("RecordFront", True) expected_files = {"rlog.zst", "qlog.zst", "qcamera.ts", "fcamera.hevc", "dcamera.hevc", "ecamera.hevc"} @@ -282,15 +282,15 @@ class TestLoggerd: sent.clear_write_flag() assert sent.to_bytes() == m.as_builder().to_bytes() - def test_preserving_flagged_segments(self): - services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) | {"userFlag"} + def test_preserving_bookmarked_segments(self): + services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) | {"userBookmark"} self._publish_random_messages(services) segment_dir = self._get_latest_log_dir() assert getxattr(segment_dir, PRESERVE_ATTR_NAME) == PRESERVE_ATTR_VALUE - def test_not_preserving_unflagged_segments(self): - services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) - {"userFlag"} + def test_not_preserving_nonbookmarked_segments(self): + services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) - {"userBookmark", "audioFeedback"} self._publish_random_messages(services) segment_dir = self._get_latest_log_dir() diff --git a/system/loggerd/uploader.py b/system/loggerd/uploader.py index 6e6df6114e..38fc0e9209 100755 --- a/system/loggerd/uploader.py +++ b/system/loggerd/uploader.py @@ -88,7 +88,7 @@ class Uploader: self.immediate_priority = {"qlog": 0, "qlog.zst": 0, "qcamera.ts": 1} def list_upload_files(self, metered: bool) -> Iterator[tuple[str, str, str]]: - r = self.params.get("AthenadRecentlyViewedRoutes", encoding="utf8") + r = self.params.get("AthenadRecentlyViewedRoutes") requested_routes = [] if r is None else [route for route in r.split(",") if route] for logdir in listdir_by_creation(self.root): @@ -238,7 +238,7 @@ def main(exit_event: threading.Event = None) -> None: clear_locks(Paths.log_root()) params = Params() - dongle_id = params.get("DongleId", encoding='utf8') + dongle_id = params.get("DongleId") if dongle_id is None: cloudlog.info("uploader missing dongle_id") diff --git a/system/loggerd/video_writer.cc b/system/loggerd/video_writer.cc index 68e870982f..1b47a8fceb 100644 --- a/system/loggerd/video_writer.cc +++ b/system/loggerd/video_writer.cc @@ -1,4 +1,3 @@ -#pragma clang diagnostic ignored "-Wdeprecated-declarations" #include #include "system/loggerd/video_writer.h" @@ -114,8 +113,7 @@ void VideoWriter::write(uint8_t *data, int len, long long timestamp, bool codecc // input timestamps are in microseconds AVRational in_timebase = {1, 1000000}; - AVPacket pkt; - av_init_packet(&pkt); + AVPacket pkt = {}; pkt.data = data; pkt.size = len; pkt.stream_index = this->out_stream->index; @@ -200,10 +198,23 @@ void VideoWriter::encode_and_write_audio_frame(AVFrame* frame) { audio_pts += audio_codec_ctx->frame_size; } +void VideoWriter::process_remaining_audio() { + // Process remaining audio samples by padding with silence + if (audio_buffer.size() > 0 && audio_buffer.size() < audio_codec_ctx->frame_size) { + audio_buffer.resize(audio_codec_ctx->frame_size, 0.0f); + + // Encode final frame + audio_frame->pts = audio_pts; + float *f_samples = reinterpret_cast(audio_frame->data[0]); + std::copy(audio_buffer.begin(), audio_buffer.end(), f_samples); + encode_and_write_audio_frame(audio_frame); + } +} VideoWriter::~VideoWriter() { if (this->remuxing) { if (this->audio_codec_ctx) { + process_remaining_audio(); encode_and_write_audio_frame(NULL); // flush encoder avcodec_free_context(&this->audio_codec_ctx); } diff --git a/system/loggerd/video_writer.h b/system/loggerd/video_writer.h index 25e6484d58..e973c5d811 100644 --- a/system/loggerd/video_writer.h +++ b/system/loggerd/video_writer.h @@ -21,6 +21,7 @@ public: private: void initialize_audio(int sample_rate); void encode_and_write_audio_frame(AVFrame* frame); + void process_remaining_audio(); std::string vid_path, lock_path; FILE *of = nullptr; diff --git a/system/manager/manager.py b/system/manager/manager.py index c59f0118aa..d1b684aa7f 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -3,12 +3,13 @@ import datetime import os import signal import sys +import time import traceback from cereal import log import cereal.messaging as messaging import openpilot.system.sentry as sentry -from openpilot.common.params import Params, ParamKeyType +from openpilot.common.params import Params, ParamKeyFlag from openpilot.common.text_window import TextWindow from openpilot.system.hardware import HARDWARE from openpilot.system.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog @@ -19,8 +20,6 @@ from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.system.version import get_build_metadata, terms_version, training_version from openpilot.system.hardware.hw import Paths -from openpilot.sunnypilot.mapd.mapd_installer import VERSION - def manager_init() -> None: save_bootlog() @@ -28,65 +27,25 @@ def manager_init() -> None: build_metadata = get_build_metadata() params = Params() - params.clear_all(ParamKeyType.CLEAR_ON_MANAGER_START) - params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION) - params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION) + params.clear_all(ParamKeyFlag.CLEAR_ON_MANAGER_START) + params.clear_all(ParamKeyFlag.CLEAR_ON_ONROAD_TRANSITION) + params.clear_all(ParamKeyFlag.CLEAR_ON_OFFROAD_TRANSITION) + params.clear_all(ParamKeyFlag.CLEAR_ON_IGNITION_ON) if build_metadata.release_channel: - params.clear_all(ParamKeyType.DEVELOPMENT_ONLY) - - default_params: list[tuple[str, str | bytes]] = [ - ("CompletedTrainingVersion", "0"), - ("DisengageOnAccelerator", "0"), - ("GsmMetered", "1"), - ("HasAcceptedTerms", "0"), - ("LanguageSetting", "main_en"), - ("OpenpilotEnabledToggle", "1"), - ("LongitudinalPersonality", str(log.LongitudinalPersonality.standard)), - ] - - sunnypilot_default_params: list[tuple[str, str | bytes]] = [ - ("AutoLaneChangeTimer", "0"), - ("AutoLaneChangeBsmDelay", "0"), - ("BlindSpot", "0"), - ("BlinkerMinLateralControlSpeed", "20"), # MPH or km/h - ("BlinkerPauseLateralControl", "0"), - ("Brightness", "0"), - ("ChevronInfo", "4"), - ("CustomAccIncrementsEnabled", "0"), - ("CustomAccLongPressIncrement", "5"), - ("CustomAccShortPressIncrement", "1"), - ("DeviceBootMode", "0"), - ("DisableUpdates", "0"), - ("DynamicExperimentalControl", "0"), - ("HyundaiLongitudinalTuning", "0"), - ("InteractivityTimeout", "0"), - ("LagdToggle", "1"), - ("LagdToggledelay", "0.2"), - ("Mads", "1"), - ("MadsMainCruiseAllowed", "1"), - ("MadsSteeringMode", "0"), - ("MadsUnifiedEngagementMode", "1"), - ("MapdVersion", f"{VERSION}"), - ("MaxTimeOffroad", "1800"), - ("ModelManager_LastSyncTime", "0"), - ("ModelManager_ModelsCache", ""), - ("NeuralNetworkLateralControl", "0"), - ("QuickBootToggle", "0"), - ("QuietMode", "0"), - ("ShowAdvancedControls", "0" if build_metadata.tested_channel else "1"), - ] + params.clear_all(ParamKeyFlag.DEVELOPMENT_ONLY) # device boot mode - if params.get("DeviceBootMode") == b"1": # start in always offroad mode + if params.get("DeviceBootMode") == 1: # start in Always Offroad mode params.put_bool("OffroadMode", True) if params.get_bool("RecordFrontLock"): params.put_bool("RecordFront", True) - # set unset params - for k, v in (default_params + sunnypilot_default_params): - if params.get(k) is None: - params.put(k, v) + # set unset params to their default value + for k in params.all_keys(): + default_value = params.get_default_value(k) + if default_value and params.get(k) is None: + params.put(k, default_value) # Create folders needed for msgq try: @@ -159,19 +118,20 @@ def manager_thread() -> None: params = Params() ignore: list[str] = [] - if params.get("DongleId", encoding='utf8') in (None, UNREGISTERED_DONGLE_ID): + if params.get("DongleId") in (None, UNREGISTERED_DONGLE_ID): ignore += ["manage_athenad", "uploader"] if os.getenv("NOBOARD") is not None: ignore.append("pandad") ignore += [x for x in os.getenv("BLOCK", "").split(",") if len(x) > 0] - sm = messaging.SubMaster(['deviceState', 'carParams'], poll='deviceState') + sm = messaging.SubMaster(['deviceState', 'carParams', 'pandaStates'], poll='deviceState') pm = messaging.PubMaster(['managerState']) write_onroad_params(False, params) ensure_running(managed_processes.values(), False, params=params, CP=sm['carParams'], not_run=ignore) started_prev = False + ignition_prev = False while True: sm.update(1000) @@ -179,15 +139,20 @@ def manager_thread() -> None: started = sm['deviceState'].started if started and not started_prev: - params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION) + params.clear_all(ParamKeyFlag.CLEAR_ON_ONROAD_TRANSITION) elif not started and started_prev: - params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION) + params.clear_all(ParamKeyFlag.CLEAR_ON_OFFROAD_TRANSITION) + + ignition = any(ps.ignitionLine or ps.ignitionCan for ps in sm['pandaStates'] if ps.pandaType != log.PandaState.PandaType.unknown) + if ignition and not ignition_prev: + params.clear_all(ParamKeyFlag.CLEAR_ON_IGNITION_ON) # update onroad params, which drives pandad's safety setter thread if started != started_prev: write_onroad_params(started, params) started_prev = started + ignition_prev = ignition ensure_running(managed_processes.values(), started, params=params, CP=sm['carParams'], not_run=ignore) @@ -201,6 +166,14 @@ def manager_thread() -> None: msg.managerState.processes = [p.get_process_state_msg() for p in managed_processes.values()] pm.send('managerState', msg) + # kick AGNOS power monitoring watchdog + try: + if sm.all_checks(['deviceState']): + with open("/var/tmp/power_watchdog", "w") as f: + f.write(str(time.monotonic())) + except Exception: + pass + # Exit main loop when uninstall/shutdown/reboot is needed shutdown = False for param in ("DoUninstall", "DoShutdown", "DoReboot"): diff --git a/system/manager/process.py b/system/manager/process.py index 0ebddae049..5e86e87c76 100644 --- a/system/manager/process.py +++ b/system/manager/process.py @@ -251,7 +251,7 @@ class DaemonProcess(ManagerProcess): if self.params is None: self.params = Params() - pid = self.params.get(self.param_name, encoding='utf-8') + pid = self.params.get(self.param_name) if pid is not None: try: os.kill(int(pid), 0) @@ -270,7 +270,7 @@ class DaemonProcess(ManagerProcess): stderr=open('/dev/null', 'w'), preexec_fn=os.setpgrp) - self.params.put(self.param_name, str(proc.pid)) + self.params.put(self.param_name, proc.pid) def stop(self, retry=True, block=True, sig=None) -> None: pass diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 6cdb89d980..c90565180c 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -117,6 +117,7 @@ procs = [ PythonProcess("sensord", "system.sensord.sensord", only_onroad, enabled=not PC), NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None)), + PythonProcess("raylib_ui", "selfdrive.ui.ui", always_run, enabled=False, watchdog_max_dt=(5 if not PC else None)), PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad), PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad), NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False), @@ -142,6 +143,7 @@ procs = [ PythonProcess("updated", "system.updated.updated", only_offroad, enabled=not PC), PythonProcess("uploader", "system.loggerd.uploader", always_run), PythonProcess("statsd", "system.statsd", always_run), + PythonProcess("feedbackd", "selfdrive.ui.feedback.feedbackd", only_onroad), # debug procs NativeProcess("bridge", "cereal/messaging", ["./bridge"], notcar), diff --git a/system/manager/test/test_manager.py b/system/manager/test/test_manager.py index 9c0b664006..5e55648283 100644 --- a/system/manager/test/test_manager.py +++ b/system/manager/test/test_manager.py @@ -38,6 +38,18 @@ class TestManager: # TODO: ensure there are blacklisted procs until we have a dedicated test assert len(BLACKLIST_PROCS), "No blacklisted procs to test not_run" + def test_set_params_with_default_value(self): + params = Params() + params.clear_all() + + os.environ['PREPAREONLY'] = '1' + manager.main() + for k in params.all_keys(): + default_value = params.get_default_value(k) + if default_value: + assert params.get(k) == default_value + assert params.get("OpenpilotEnabledToggle") + @pytest.mark.skip("this test is flaky the way it's currently written, should be moved to test_onroad") def test_clean_exit(self, subtests): """ diff --git a/system/qcomgpsd/qcomgpsd.py b/system/qcomgpsd/qcomgpsd.py index 3a9e3585ba..819b17f113 100755 --- a/system/qcomgpsd/qcomgpsd.py +++ b/system/qcomgpsd/qcomgpsd.py @@ -286,7 +286,7 @@ def main() -> NoReturn: continue if DEBUG: - print(f"{time.time():.4f}: got log: {log_type} len {len(log_payload)}") + print(f"{time.time():.4f}: got log: {log_type} len {len(log_payload)}") # noqa: TID251 if log_type == LOG_GNSS_OEMDRE_MEASUREMENT_REPORT: msg = messaging.new_message('qcomGnss', valid=True) diff --git a/system/sensord/sensord.py b/system/sensord/sensord.py index ed9d0ed415..2b6467fa78 100755 --- a/system/sensord/sensord.py +++ b/system/sensord/sensord.py @@ -98,6 +98,13 @@ def main() -> None: (MMC5603NJ_Magn(I2C_BUS_IMU), "magnetometer", False), ] + # Reset sensors + for sensor, _, _ in sensors_cfg: + try: + sensor.reset() + except Exception: + cloudlog.exception(f"Error initializing {sensor} sensor") + # Initialize sensors exit_event = threading.Event() threads = [ diff --git a/system/sensord/sensors/i2c_sensor.py b/system/sensord/sensors/i2c_sensor.py index 0e15a6622b..336ebb1fd3 100644 --- a/system/sensord/sensors/i2c_sensor.py +++ b/system/sensord/sensors/i2c_sensor.py @@ -40,6 +40,11 @@ class Sensor: def device_address(self) -> int: raise NotImplementedError + def reset(self) -> None: + # optional. + # not part of init due to shared registers + pass + def init(self) -> None: raise NotImplementedError diff --git a/system/sensord/sensors/lsm6ds3_accel.py b/system/sensord/sensors/lsm6ds3_accel.py index 2d788fcbe2..43863daa93 100644 --- a/system/sensord/sensors/lsm6ds3_accel.py +++ b/system/sensord/sensors/lsm6ds3_accel.py @@ -31,6 +31,10 @@ class LSM6DS3_Accel(Sensor): def device_address(self) -> int: return 0x6A + def reset(self): + self.write(0x12, 0x1) + time.sleep(0.1) + def init(self): chip_id = self.verify_chip_id(0x0F, [0x69, 0x6A]) if chip_id == 0x6A: diff --git a/system/sensord/sensors/lsm6ds3_gyro.py b/system/sensord/sensors/lsm6ds3_gyro.py index 68fd267df2..60de2bbe02 100644 --- a/system/sensord/sensors/lsm6ds3_gyro.py +++ b/system/sensord/sensors/lsm6ds3_gyro.py @@ -29,6 +29,10 @@ class LSM6DS3_Gyro(Sensor): def device_address(self) -> int: return 0x6A + def reset(self): + self.write(0x12, 0x1) + time.sleep(0.1) + def init(self): chip_id = self.verify_chip_id(0x0F, [0x69, 0x6A]) if chip_id == 0x6A: diff --git a/system/sentry.py b/system/sentry.py index 2f918ebc49..a1fb604dea 100644 --- a/system/sentry.py +++ b/system/sentry.py @@ -106,10 +106,10 @@ def set_user() -> None: def get_properties() -> tuple[str, str, str]: params = Params() - hardware_serial: str = params.get("HardwareSerial", encoding='utf-8') or "" - git_username: str = params.get("GithubUsername", encoding='utf-8') or "" - dongle_id: str = params.get("DongleId", encoding='utf-8') or f"{UNREGISTERED_DONGLE_ID}-{hardware_serial}" - sunnylink_dongle_id: str = params.get("SunnylinkDongleId", encoding='utf-8') or UNREGISTERED_SUNNYLINK_DONGLE_ID + hardware_serial: str = params.get("HardwareSerial") or "" + git_username: str = params.get("GithubUsername") or "" + dongle_id: str = params.get("DongleId") or f"{UNREGISTERED_DONGLE_ID}-{hardware_serial}" + sunnylink_dongle_id: str = params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID return dongle_id, git_username, sunnylink_dongle_id diff --git a/system/statsd.py b/system/statsd.py index b8a7f2c661..d60064fc91 100755 --- a/system/statsd.py +++ b/system/statsd.py @@ -61,7 +61,7 @@ class StatLog: def main() -> NoReturn: - dongle_id = Params().get("DongleId", encoding='utf-8') + dongle_id = Params().get("DongleId") def get_influxdb_line(measurement: str, value: float | dict[str, float], timestamp: datetime, tags: dict) -> str: res = f"{measurement}" for k, v in tags.items(): diff --git a/system/ubloxd/pigeond.py b/system/ubloxd/pigeond.py index e6a03ffbdf..e458a9d65f 100755 --- a/system/ubloxd/pigeond.py +++ b/system/ubloxd/pigeond.py @@ -41,7 +41,7 @@ def add_ubx_checksum(msg: bytes) -> bytes: B = (B + A) % 256 return msg + bytes([A, B]) -def get_assistnow_messages(token: bytes) -> list[bytes]: +def get_assistnow_messages(token: str) -> list[bytes]: # make request # TODO: implement adding the last known location r = requests.get("https://online-live2.services.u-blox.com/GetOnlineData.ashx", params=urllib.parse.urlencode({ @@ -157,7 +157,7 @@ def init_baudrate(pigeon: TTYPigeon): pigeon.set_baud(460800) -def initialize_pigeon(pigeon: TTYPigeon) -> bool: +def init_pigeon(pigeon: TTYPigeon) -> bool: # try initializing a few times for _ in range(10): try: @@ -250,8 +250,6 @@ def initialize_pigeon(pigeon: TTYPigeon) -> bool: return True def deinitialize_and_exit(pigeon: TTYPigeon | None): - cloudlog.warning("Storing almanac in ublox flash") - if pigeon is not None: # controlled GNSS stop pigeon.send(b"\xB5\x62\x06\x04\x04\x00\x00\x00\x08\x00\x16\x74") @@ -260,12 +258,9 @@ def deinitialize_and_exit(pigeon: TTYPigeon | None): set_power(False) sys.exit(0) -def create_pigeon() -> tuple[TTYPigeon, messaging.PubMaster]: - pigeon = None - +def init(pigeon: TTYPigeon) -> None: # register exit handler signal.signal(signal.SIGINT, lambda sig, frame: deinitialize_and_exit(pigeon)) - pm = messaging.PubMaster(['ubloxRaw']) # power cycle ublox set_power(False) @@ -273,23 +268,23 @@ def create_pigeon() -> tuple[TTYPigeon, messaging.PubMaster]: set_power(True) time.sleep(0.5) - pigeon = TTYPigeon() - return pigeon, pm + init_baudrate(pigeon) + init_pigeon(pigeon) -def run_receiving(pigeon: TTYPigeon, pm: messaging.PubMaster, duration: int = 0): +def run_receiving(duration: int = 0): + pm = messaging.PubMaster(['ubloxRaw']) + + pigeon = TTYPigeon() + init(pigeon) start_time = time.monotonic() - def end_condition(): - return True if duration == 0 else time.monotonic() - start_time < duration - last_almanac_save = time.monotonic() - while end_condition(): + while (duration == 0) or (time.monotonic() - start_time < duration): 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) + init(pigeon) continue # send out to socket @@ -308,13 +303,7 @@ def run_receiving(pigeon: TTYPigeon, pm: messaging.PubMaster, duration: int = 0) def main(): assert TICI, "unsupported hardware for pigeond" - - pigeon, pm = create_pigeon() - init_baudrate(pigeon) - initialize_pigeon(pigeon) - - # start receiving data - run_receiving(pigeon, pm) + run_receiving() if __name__ == "__main__": main() diff --git a/system/ui/README.md b/system/ui/README.md index 85d32bfd6c..b124ae4d85 100644 --- a/system/ui/README.md +++ b/system/ui/README.md @@ -8,3 +8,8 @@ Quick start: * set `SCALE=1.5` to scale the entire UI by 1.5x * https://www.raylib.com/cheatsheet/cheatsheet.html * https://electronstudio.github.io/raylib-python-cffi/README.html#quickstart + +Style guide: +* All graphical elements should subclass [`Widget`](/system/ui/widgets/__init__.py). + * Prefer a stateful widget over a function for easy migration from QT +* All internal class variables and functions should be prefixed with `_` diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 12d825b99f..718bf036fa 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -7,7 +7,7 @@ import threading from collections.abc import Callable from collections import deque from dataclasses import dataclass -from enum import IntEnum +from enum import StrEnum from typing import NamedTuple from importlib.resources import as_file, files from openpilot.common.swaglog import cloudlog @@ -19,6 +19,7 @@ FPS_LOG_INTERVAL = 5 # Seconds between logging FPS drops FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions MOUSE_THREAD_RATE = 140 # touch controller runs at 140Hz +MAX_TOUCH_SLOTS = 2 ENABLE_VSYNC = os.getenv("ENABLE_VSYNC", "0") == "1" SHOW_FPS = os.getenv("SHOW_FPS") == "1" @@ -33,16 +34,16 @@ ASSETS_DIR = files("openpilot.selfdrive").joinpath("assets") FONT_DIR = ASSETS_DIR.joinpath("fonts") -class FontWeight(IntEnum): - THIN = 0 - EXTRA_LIGHT = 1 - LIGHT = 2 - NORMAL = 3 - MEDIUM = 4 - SEMI_BOLD = 5 - BOLD = 6 - EXTRA_BOLD = 7 - BLACK = 8 +class FontWeight(StrEnum): + THIN = "Inter-Thin.ttf" + EXTRA_LIGHT = "Inter-ExtraLight.ttf" + LIGHT = "Inter-Light.ttf" + NORMAL = "Inter-Regular.ttf" + MEDIUM = "Inter-Medium.ttf" + SEMI_BOLD = "Inter-SemiBold.ttf" + BOLD = "Inter-Bold.ttf" + EXTRA_BOLD = "Inter-ExtraBold.ttf" + BLACK = "Inter-Black.ttf" @dataclass @@ -58,6 +59,7 @@ class MousePos(NamedTuple): class MouseEvent(NamedTuple): pos: MousePos + slot: int left_pressed: bool left_released: bool left_down: bool @@ -67,7 +69,7 @@ class MouseEvent(NamedTuple): class MouseState: def __init__(self): self._events: deque[MouseEvent] = deque(maxlen=MOUSE_THREAD_RATE) # bound event list - self._prev_mouse_event: MouseEvent | None = None + self._prev_mouse_event: list[MouseEvent | None] = [None] * MAX_TOUCH_SLOTS self._rk = Ratekeeper(MOUSE_THREAD_RATE) self._lock = threading.Lock() @@ -98,19 +100,21 @@ class MouseState: self._rk.keep_time() def _handle_mouse_event(self): - mouse_pos = rl.get_mouse_position() - ev = MouseEvent( - MousePos(mouse_pos.x, mouse_pos.y), - rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT), - rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT), - rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT), - time.monotonic(), - ) - # Only add changes - if self._prev_mouse_event is None or ev[:-1] != self._prev_mouse_event[:-1]: - with self._lock: - self._events.append(ev) - self._prev_mouse_event = ev + for slot in range(MAX_TOUCH_SLOTS): + mouse_pos = rl.get_touch_position(slot) + ev = MouseEvent( + MousePos(mouse_pos.x, mouse_pos.y), + slot, + rl.is_mouse_button_pressed(slot), + rl.is_mouse_button_released(slot), + rl.is_mouse_button_down(slot), + time.monotonic(), + ) + # Only add changes + if self._prev_mouse_event[slot] is None or ev[:-1] != self._prev_mouse_event[slot][:-1]: + with self._lock: + self._events.append(ev) + self._prev_mouse_event[slot] = ev class GuiApplication: @@ -312,34 +316,23 @@ class GuiApplication: return self._height def _load_fonts(self): - font_files = ( - "Inter-Thin.ttf", - "Inter-ExtraLight.ttf", - "Inter-Light.ttf", - "Inter-Regular.ttf", - "Inter-Medium.ttf", - "Inter-SemiBold.ttf", - "Inter-Bold.ttf", - "Inter-ExtraBold.ttf", - "Inter-Black.ttf", - ) - # Create a character set from our keyboard layouts from openpilot.system.ui.widgets.keyboard import KEYBOARD_LAYOUTS + all_chars = set() for layout in KEYBOARD_LAYOUTS.values(): all_chars.update(key for row in layout for key in row) all_chars = "".join(all_chars) - all_chars += "–✓°" + all_chars += "–✓×°" codepoint_count = rl.ffi.new("int *", 1) codepoints = rl.load_codepoints(all_chars, codepoint_count) - for index, font_file in enumerate(font_files): - with as_file(FONT_DIR.joinpath(font_file)) as fspath: + for font_weight_file in FontWeight: + with as_file(FONT_DIR.joinpath(font_weight_file)) as fspath: font = rl.load_font_ex(fspath.as_posix(), 200, codepoints, codepoint_count[0]) rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) - self._fonts[index] = font + self._fonts[font_weight_file] = font rl.unload_codepoints(codepoints) rl.gui_set_font(self._fonts[FontWeight.NORMAL]) diff --git a/system/ui/lib/emoji.py b/system/ui/lib/emoji.py new file mode 100644 index 0000000000..28139158a1 --- /dev/null +++ b/system/ui/lib/emoji.py @@ -0,0 +1,47 @@ +import io +import re + +from PIL import Image, ImageDraw, ImageFont +import pyray as rl + +_cache: dict[str, rl.Texture] = {} + +EMOJI_REGEX = re.compile( +"""[\U0001F600-\U0001F64F +\U0001F300-\U0001F5FF +\U0001F680-\U0001F6FF +\U0001F1E0-\U0001F1FF +\U00002700-\U000027BF +\U0001F900-\U0001F9FF +\U00002600-\U000026FF +\U00002300-\U000023FF +\U00002B00-\U00002BFF +\U0001FA70-\U0001FAFF +\U0001F700-\U0001F77F +\u2640-\u2642 +\u2600-\u2B55 +\u200d +\u23cf +\u23e9 +\u231a +\ufe0f +\u3030 +]+""", + flags=re.UNICODE +) + +def find_emoji(text): + return [(m.start(), m.end(), m.group()) for m in EMOJI_REGEX.finditer(text)] + +def emoji_tex(emoji): + if emoji not in _cache: + img = Image.new("RGBA", (128, 128), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + font = ImageFont.truetype("NotoColorEmoji", 109) + draw.text((0, 0), emoji, font=font, embedded_color=True) + buffer = io.BytesIO() + img.save(buffer, format="PNG") + l = buffer.tell() + buffer.seek(0) + _cache[emoji] = rl.load_texture_from_image(rl.load_image_from_memory(".png", buffer.getvalue(), l)) + return _cache[emoji] diff --git a/system/ui/lib/label.py b/system/ui/lib/label.py deleted file mode 100644 index c3d0e0303a..0000000000 --- a/system/ui/lib/label.py +++ /dev/null @@ -1,78 +0,0 @@ -import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR -from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.utils import GuiStyleContext - - -def gui_label( - rect: rl.Rectangle, - text: str, - font_size: int = DEFAULT_TEXT_SIZE, - color: rl.Color = DEFAULT_TEXT_COLOR, - font_weight: FontWeight = FontWeight.NORMAL, - alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, - elide_right: bool = True -): - font = gui_app.font(font_weight) - text_size = measure_text_cached(font, text, font_size) - display_text = text - - # Elide text to fit within the rectangle - if elide_right and text_size.x > rect.width: - ellipsis = "..." - left, right = 0, len(text) - while left < right: - mid = (left + right) // 2 - candidate = text[:mid] + ellipsis - candidate_size = measure_text_cached(font, candidate, font_size) - if candidate_size.x <= rect.width: - left = mid + 1 - else: - right = mid - display_text = text[: left - 1] + ellipsis if left > 0 else ellipsis - text_size = measure_text_cached(font, display_text, font_size) - - # Calculate horizontal position based on alignment - text_x = rect.x + { - rl.GuiTextAlignment.TEXT_ALIGN_LEFT: 0, - rl.GuiTextAlignment.TEXT_ALIGN_CENTER: (rect.width - text_size.x) / 2, - rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: rect.width - text_size.x, - }.get(alignment, 0) - - # Calculate vertical position based on alignment - text_y = rect.y + { - rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: 0, - rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: (rect.height - text_size.y) / 2, - rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: rect.height - text_size.y, - }.get(alignment_vertical, 0) - - # Draw the text in the specified rectangle - rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color) - - -def gui_text_box( - rect: rl.Rectangle, - text: str, - font_size: int = DEFAULT_TEXT_SIZE, - color: rl.Color = DEFAULT_TEXT_COLOR, - alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, - font_weight: FontWeight = FontWeight.NORMAL, -): - styles = [ - (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(color)), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, font_size), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_LINE_SPACING, font_size), - (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_ALIGNMENT, alignment), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_ALIGNMENT_VERTICAL, alignment_vertical), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_WRAP_MODE, rl.GuiTextWrapMode.TEXT_WRAP_WORD) - ] - if font_weight != FontWeight.NORMAL: - rl.gui_set_font(gui_app.font(font_weight)) - - with GuiStyleContext(styles): - rl.gui_label(rect, text) - - if font_weight != FontWeight.NORMAL: - rl.gui_set_font(gui_app.font(FontWeight.NORMAL)) diff --git a/system/ui/lib/scroll_panel.py b/system/ui/lib/scroll_panel.py index 21b6795a75..e2296fd5ed 100644 --- a/system/ui/lib/scroll_panel.py +++ b/system/ui/lib/scroll_panel.py @@ -41,8 +41,9 @@ class GuiScrollPanel: def handle_scroll(self, bounds: rl.Rectangle, content: rl.Rectangle) -> rl.Vector2: # TODO: HACK: this class is driven by mouse events, so we need to ensure we have at least one event to process - for mouse_event in gui_app.mouse_events or [MouseEvent(MousePos(0, 0), False, False, False, time.monotonic())]: - self._handle_mouse_event(mouse_event, bounds, content) + for mouse_event in gui_app.mouse_events or [MouseEvent(MousePos(0, 0), 0, False, False, False, time.monotonic())]: + if mouse_event.slot == 0: + self._handle_mouse_event(mouse_event, bounds, content) return self._offset def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, content: rl.Rectangle): diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 75d1bdbfd3..5d0f84bc86 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -91,7 +91,7 @@ class WifiManager: # Set tethering ssid as "weedle" + first 4 characters of a dongle id self._tethering_ssid = "weedle" if Params is not None: - dongle_id = Params().get("DongleId", encoding="utf-8") + dongle_id = Params().get("DongleId") if dongle_id: self._tethering_ssid += "-" + dongle_id[:4] self.running: bool = True @@ -441,33 +441,33 @@ class WifiManager: settings_iface.on_connection_removed(self._on_connection_removed) def _on_properties_changed(self, interface: str, changed: dict, invalidated: list): - # print("property changed", interface, changed, invalidated) - if 'LastScan' in changed: + if interface == NM_WIRELESS_IFACE and 'LastScan' in changed: asyncio.create_task(self._refresh_networks()) elif interface == NM_WIRELESS_IFACE and "ActiveAccessPoint" in changed: new_ap_path = changed["ActiveAccessPoint"].value if self.active_ap_path != new_ap_path: self.active_ap_path = new_ap_path - asyncio.create_task(self._refresh_networks()) def _on_state_changed(self, new_state: int, old_state: int, reason: int): - print("State changed", new_state, old_state, reason) if new_state == NMDeviceState.ACTIVATED: if self.callbacks.activated: self.callbacks.activated() - asyncio.create_task(self._refresh_networks()) self._current_connection_ssid = None + asyncio.create_task(self._refresh_networks()) elif new_state in (NMDeviceState.DISCONNECTED, NMDeviceState.NEED_AUTH): for network in self.networks: network.is_connected = False + # BAD PASSWORD if new_state == NMDeviceState.NEED_AUTH and reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and self.callbacks.need_auth: if self._current_connection_ssid: + asyncio.create_task(self.forget_connection(self._current_connection_ssid)) self.callbacks.need_auth(self._current_connection_ssid) else: # Try to find the network from active_ap_path for network in self.networks: if network.path == self.active_ap_path: + asyncio.create_task(self.forget_connection(network.ssid)) self.callbacks.need_auth(network.ssid) break else: @@ -486,9 +486,6 @@ class WifiManager: if self.callbacks.forgotten: self.callbacks.forgotten(ssid) - - # Update network list to reflect the removed saved connection - asyncio.create_task(self._refresh_networks()) break async def _add_saved_connection(self, path: str) -> None: @@ -497,14 +494,13 @@ class WifiManager: settings = await self._get_connection_settings(path) if ssid := self._extract_ssid(settings): self.saved_connections[ssid] = path - await self._refresh_networks() except DBusError as e: cloudlog.error(f"Failed to add connection {path}: {e}") def _extract_ssid(self, settings: dict) -> str | None: """Extract SSID from connection settings.""" ssid_variant = settings.get('802-11-wireless', {}).get('ssid', Variant('ay', b'')).value - return ''.join(chr(b) for b in ssid_variant) if ssid_variant else None + return bytes(ssid_variant).decode('utf-8') if ssid_variant else None async def _add_match_rule(self, rule): """Add a match rule on the bus.""" @@ -534,7 +530,7 @@ class WifiManager: props_iface = await self._get_interface(NM, ap_path, NM_PROPERTIES_IFACE) properties = await props_iface.call_get_all('org.freedesktop.NetworkManager.AccessPoint') ssid_variant = properties['Ssid'].value - ssid = ''.join(chr(byte) for byte in ssid_variant) + ssid = bytes(ssid_variant).decode('utf-8') if not ssid: continue @@ -543,18 +539,28 @@ class WifiManager: flags = properties['Flags'].value wpa_flags = properties['WpaFlags'].value rsn_flags = properties['RsnFlags'].value - existing_network = network_dict.get(ssid) - if not existing_network or ((not existing_network.bssid and bssid) or (existing_network.strength < strength)): + + # May be multiple access points for each SSID. Use first for ssid + # and security type, then update the rest using all APs + if ssid not in network_dict: network_dict[ssid] = NetworkInfo( ssid=ssid, - strength=strength, + strength=0, security_type=self._get_security_type(flags, wpa_flags, rsn_flags), - path=ap_path, - bssid=bssid, - is_connected=self.active_ap_path == ap_path and self._current_connection_ssid != ssid, + path="", + bssid="", + is_connected=False, is_saved=ssid in self.saved_connections ) + existing_network = network_dict.get(ssid) + if existing_network.strength < strength: + existing_network.strength = strength + existing_network.path = ap_path + existing_network.bssid = bssid + if self.active_ap_path == ap_path: + existing_network.is_connected = self._current_connection_ssid != ssid + except DBusError as e: cloudlog.error(f"Error fetching networks: {e}") except Exception as e: diff --git a/system/ui/reset.py b/system/ui/reset.py index c0dbd1d956..b1bf5a6b6a 100755 --- a/system/ui/reset.py +++ b/system/ui/reset.py @@ -1,18 +1,21 @@ #!/usr/bin/env python3 import os -import pyray as rl import sys import threading +import time from enum import IntEnum +import pyray as rl + from openpilot.system.hardware import PC from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.label import gui_label, gui_text_box -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import Button, ButtonStyle +from openpilot.system.ui.widgets.label import gui_label, gui_text_box NVME = "/dev/nvme0n1" USERDATA = "/dev/disk/by-partlabel/userdata" +TIMEOUT = 3*60 class ResetMode(IntEnum): @@ -31,8 +34,16 @@ class ResetState(IntEnum): class Reset(Widget): def __init__(self, mode): super().__init__() - self.mode = mode - self.reset_state = ResetState.NONE + self._mode = mode + self._previous_reset_state = None + self._reset_state = ResetState.NONE + self._cancel_button = Button("Cancel", self._cancel_callback) + self._confirm_button = Button("Confirm", self._confirm, button_style=ButtonStyle.PRIMARY) + self._reboot_button = Button("Reboot", lambda: os.system("sudo reboot")) + self._render_status = True + + def _cancel_callback(self): + self._render_status = False def _do_erase(self): if PC: @@ -50,53 +61,58 @@ class Reset(Widget): if rm == 0 or fmt == 0: os.system("sudo reboot") else: - self.reset_state = ResetState.FAILED + self._reset_state = ResetState.FAILED def start_reset(self): - self.reset_state = ResetState.RESETTING + self._reset_state = ResetState.RESETTING threading.Timer(0.1, self._do_erase).start() + def _update_state(self): + if self._reset_state != self._previous_reset_state: + self._previous_reset_state = self._reset_state + self._timeout_st = time.monotonic() + elif self._reset_state != ResetState.RESETTING and (time.monotonic() - self._timeout_st) > TIMEOUT: + exit(0) + def _render(self, rect: rl.Rectangle): label_rect = rl.Rectangle(rect.x + 140, rect.y, rect.width - 280, 100) gui_label(label_rect, "System Reset", 100, font_weight=FontWeight.BOLD) text_rect = rl.Rectangle(rect.x + 140, rect.y + 140, rect.width - 280, rect.height - 90 - 100) - gui_text_box(text_rect, self.get_body_text(), 90) + gui_text_box(text_rect, self._get_body_text(), 90) button_height = 160 button_spacing = 50 button_top = rect.y + rect.height - button_height button_width = (rect.width - button_spacing) / 2.0 - if self.reset_state != ResetState.RESETTING: - if self.mode == ResetMode.RECOVER or self.reset_state == ResetState.FAILED: - if gui_button(rl.Rectangle(rect.x, button_top, button_width, button_height), "Reboot"): - os.system("sudo reboot") - elif self.mode == ResetMode.USER_RESET: - if gui_button(rl.Rectangle(rect.x, button_top, button_width, button_height), "Cancel"): - return False + if self._reset_state != ResetState.RESETTING: + if self._mode == ResetMode.RECOVER: + self._reboot_button.render(rl.Rectangle(rect.x, button_top, button_width, button_height)) + elif self._mode == ResetMode.USER_RESET: + self._cancel_button.render(rl.Rectangle(rect.x, button_top, button_width, button_height)) - if self.reset_state != ResetState.FAILED: - if gui_button(rl.Rectangle(rect.x + button_width + 50, button_top, button_width, button_height), - "Confirm", button_style=ButtonStyle.PRIMARY): - self.confirm() + if self._reset_state != ResetState.FAILED: + self._confirm_button.render(rl.Rectangle(rect.x + button_width + 50, button_top, button_width, button_height)) + else: + self._reboot_button.render(rl.Rectangle(rect.x, button_top, rect.width, button_height)) - return True + return self._render_status - def confirm(self): - if self.reset_state == ResetState.CONFIRM: + def _confirm(self): + if self._reset_state == ResetState.CONFIRM: self.start_reset() else: - self.reset_state = ResetState.CONFIRM + self._reset_state = ResetState.CONFIRM - def get_body_text(self): - if self.reset_state == ResetState.CONFIRM: + def _get_body_text(self): + if self._reset_state == ResetState.CONFIRM: return "Are you sure you want to reset your device?" - if self.reset_state == ResetState.RESETTING: + if self._reset_state == ResetState.RESETTING: return "Resetting device...\nThis may take up to a minute." - if self.reset_state == ResetState.FAILED: + if self._reset_state == ResetState.FAILED: return "Reset failed. Reboot to try again." - if self.mode == ResetMode.RECOVER: + if self._mode == ResetMode.RECOVER: return "Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device." return "System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot." @@ -109,7 +125,7 @@ def main(): elif sys.argv[1] == "--format": mode = ResetMode.FORMAT - gui_app.init_window("System Reset") + gui_app.init_window("System Reset", 20) reset = Reset(mode) if mode == ResetMode.FORMAT: diff --git a/system/ui/setup.py b/system/ui/setup.py index 4d9d269480..d675e868ff 100755 --- a/system/ui/setup.py +++ b/system/ui/setup.py @@ -4,17 +4,18 @@ import re import threading import time import urllib.request +from urllib.parse import urlparse from enum import IntEnum import pyray as rl from cereal import log from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.label import gui_label, gui_text_box -from openpilot.system.ui.lib.widget import Widget -from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManagerWrapper +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio from openpilot.system.ui.widgets.keyboard import Keyboard +from openpilot.system.ui.widgets.label import Label, TextAlignment +from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManagerWrapper NetworkType = log.DeviceState.NetworkType @@ -35,9 +36,10 @@ class SetupState(IntEnum): GETTING_STARTED = 1 NETWORK_SETUP = 2 SOFTWARE_SELECTION = 3 - CUSTOM_URL = 4 + CUSTOM_SOFTWARE = 4 DOWNLOADING = 5 DOWNLOAD_FAILED = 6 + CUSTOM_SOFTWARE_WARNING = 7 class Setup(Widget): @@ -57,10 +59,50 @@ class Setup(Widget): self.wifi_ui = WifiManagerUI(self.wifi_manager) self.keyboard = Keyboard() self.selected_radio = None - self.warning = gui_app.texture("icons/warning.png", 150, 150) self.checkmark = gui_app.texture("icons/circled_check.png", 100, 100) + self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, TextAlignment.LEFT, text_color=rl.Color(255, 89, 79, 255)) + self._low_voltage_body_label = Label("Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE, + text_alignment=TextAlignment.LEFT) + self._low_voltage_continue_button = Button("Continue", self._low_voltage_continue_button_callback) + self._low_voltage_poweroff_button = Button("Power Off", HARDWARE.shutdown) + + self._getting_started_button = Button("", self._getting_started_button_callback, button_style=ButtonStyle.PRIMARY, border_radius=0) + self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + self._getting_started_body_label = Label("Before we get on the road, let's finish installation and cover some details.", + BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT) + + self._software_selection_openpilot_button = ButtonRadio("openpilot", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80) + self._software_selection_custom_software_button = ButtonRadio("Custom Software", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80) + self._software_selection_continue_button = Button("Continue", self._software_selection_continue_button_callback, + button_style=ButtonStyle.PRIMARY) + self._software_selection_continue_button.set_enabled(False) + self._software_selection_back_button = Button("Back", self._software_selection_back_button_callback) + self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + + self._download_failed_reboot_button = Button("Reboot device", HARDWARE.reboot) + self._download_failed_startover_button = Button("Start over", self._download_failed_startover_button_callback, button_style=ButtonStyle.PRIMARY) + self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + self._download_failed_url_label = Label("", 64, FontWeight.NORMAL, TextAlignment.LEFT) + self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT) + + self._network_setup_back_button = Button("Back", self._network_setup_back_button_callback) + self._network_setup_continue_button = Button("Waiting for internet", self._network_setup_continue_button_callback, + button_style=ButtonStyle.PRIMARY) + self._network_setup_continue_button.set_enabled(False) + self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + + self._custom_software_warning_continue_button = Button("Continue", self._custom_software_warning_continue_button_callback) + self._custom_software_warning_back_button = Button("Back", self._custom_software_warning_back_button_callback) + self._custom_software_warning_title_label = Label("WARNING: Custom Software", 100, FontWeight.BOLD, TextAlignment.LEFT, text_color=rl.Color(255,89,79,255), + text_padding=60) + self._custom_software_warning_body_label = Label("Use caution when installing third-party software. Third-party software has not been tested by comma," + + " and may cause damage to your device and/or vehicle.\n\nIf you'd like to proceed, use https://flash.comma.ai " + + "to restore your device to a factory state later.", + 85, text_alignment=TextAlignment.LEFT, text_padding=60) + self._downloading_body_label = Label("Downloading...", TITLE_FONT_SIZE, FontWeight.MEDIUM) + try: with open("/sys/class/hwmon/hwmon1/in1_input") as f: voltage = float(f.read().strip()) / 1000.0 @@ -78,48 +120,70 @@ class Setup(Widget): self.render_network_setup(rect) elif self.state == SetupState.SOFTWARE_SELECTION: self.render_software_selection(rect) - elif self.state == SetupState.CUSTOM_URL: - self.render_custom_url() + elif self.state == SetupState.CUSTOM_SOFTWARE_WARNING: + self.render_custom_software_warning(rect) + elif self.state == SetupState.CUSTOM_SOFTWARE: + self.render_custom_software() elif self.state == SetupState.DOWNLOADING: self.render_downloading(rect) elif self.state == SetupState.DOWNLOAD_FAILED: self.render_download_failed(rect) + def _low_voltage_continue_button_callback(self): + self.state = SetupState.GETTING_STARTED + + def _custom_software_warning_back_button_callback(self): + self.state = SetupState.SOFTWARE_SELECTION + + def _custom_software_warning_continue_button_callback(self): + self.state = SetupState.CUSTOM_SOFTWARE + + def _getting_started_button_callback(self): + self.state = SetupState.NETWORK_SETUP + self.stop_network_check_thread.clear() + self.start_network_check() + + def _software_selection_back_button_callback(self): + self.state = SetupState.NETWORK_SETUP + self.stop_network_check_thread.clear() + self.start_network_check() + + def _software_selection_continue_button_callback(self): + if self._software_selection_openpilot_button.selected: + self.download(OPENPILOT_URL) + else: + self.state = SetupState.CUSTOM_SOFTWARE_WARNING + + def _download_failed_startover_button_callback(self): + self.state = SetupState.GETTING_STARTED + + def _network_setup_back_button_callback(self): + self.state = SetupState.GETTING_STARTED + + def _network_setup_continue_button_callback(self): + self.state = SetupState.SOFTWARE_SELECTION + self.stop_network_check_thread.set() + def render_low_voltage(self, rect: rl.Rectangle): rl.draw_texture(self.warning, int(rect.x + 150), int(rect.y + 110), rl.WHITE) - title_rect = rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 100, rect.width - 500 - 150, TITLE_FONT_SIZE) - gui_label(title_rect, "WARNING: Low Voltage", TITLE_FONT_SIZE, rl.Color(255, 89, 79, 255), FontWeight.MEDIUM) - - body_rect = rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 100 + TITLE_FONT_SIZE + 25, rect.width - 500 - 150, BODY_FONT_SIZE * 3) - gui_text_box(body_rect, "Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE) + self._low_voltage_title_label.render(rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 100, rect.width - 500 - 150, TITLE_FONT_SIZE)) + self._low_voltage_body_label.render(rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 150, rect.width - 500, BODY_FONT_SIZE * 3)) button_width = (rect.width - MARGIN * 3) / 2 button_y = rect.height - MARGIN - BUTTON_HEIGHT - - if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Power off"): - HARDWARE.shutdown() - - if gui_button(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT), "Continue"): - self.state = SetupState.GETTING_STARTED + self._low_voltage_poweroff_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) + self._low_voltage_continue_button.render(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT)) def render_getting_started(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x + 165, rect.y + 280, rect.width - 265, TITLE_FONT_SIZE) - gui_label(title_rect, "Getting Started", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) - - desc_rect = rl.Rectangle(rect.x + 165, rect.y + 280 + TITLE_FONT_SIZE + 90, rect.width - 500, BODY_FONT_SIZE * 3) - gui_text_box(desc_rect, "Before we get on the road, let's finish installation and cover some details.", BODY_FONT_SIZE) + self._getting_started_title_label.render(rl.Rectangle(rect.x + 165, rect.y + 280, rect.width - 265, TITLE_FONT_SIZE)) + self._getting_started_body_label.render(rl.Rectangle(rect.x + 165, rect.y + 280 + TITLE_FONT_SIZE, rect.width - 500, BODY_FONT_SIZE * 3)) btn_rect = rl.Rectangle(rect.width - NEXT_BUTTON_WIDTH, 0, NEXT_BUTTON_WIDTH, rect.height) - - ret = gui_button(btn_rect, "", button_style=ButtonStyle.PRIMARY, border_radius=0) + self._getting_started_button.render(btn_rect) triangle = gui_app.texture("images/button_continue_triangle.png", 54, int(btn_rect.height)) rl.draw_texture_v(triangle, rl.Vector2(btn_rect.x + btn_rect.width / 2 - triangle.width / 2, btn_rect.height / 2 - triangle.height / 2), rl.WHITE) - if ret: - self.state = SetupState.NETWORK_SETUP - self.start_network_check() - def check_network_connectivity(self): while not self.stop_network_check_thread.is_set(): if self.state == SetupState.NETWORK_SETUP: @@ -145,8 +209,7 @@ class Setup(Widget): self.network_check_thread.join() def render_network_setup(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE) - gui_label(title_rect, "Connect to Wi-Fi", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) + self._network_setup_title_label.render(rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE)) wifi_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN + 25, rect.width - MARGIN * 2, rect.height - TITLE_FONT_SIZE - 25 - BUTTON_HEIGHT - MARGIN * 3) @@ -157,102 +220,69 @@ class Setup(Widget): button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2 button_y = rect.height - BUTTON_HEIGHT - MARGIN - if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Back"): - self.state = SetupState.GETTING_STARTED + self._network_setup_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) # Check network connectivity status continue_enabled = self.network_connected.is_set() + self._network_setup_continue_button.set_enabled(continue_enabled) continue_text = ("Continue" if self.wifi_connected.is_set() else "Continue without Wi-Fi") if continue_enabled else "Waiting for internet" - - if gui_button( - rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT), - continue_text, - button_style=ButtonStyle.PRIMARY if continue_enabled else ButtonStyle.NORMAL, - is_enabled=continue_enabled, - ): - self.state = SetupState.SOFTWARE_SELECTION - self.stop_network_check_thread.set() + self._network_setup_continue_button.set_text(continue_text) + self._network_setup_continue_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT)) def render_software_selection(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE) - gui_label(title_rect, "Choose Software to Install", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) + self._software_selection_title_label.render(rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE)) radio_height = 230 radio_spacing = 30 + self._software_selection_continue_button.set_enabled(False) + openpilot_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN * 2, rect.width - MARGIN * 2, radio_height) - openpilot_selected = self.selected_radio == "openpilot" + self._software_selection_openpilot_button.render(openpilot_rect) - rl.draw_rectangle_rounded(openpilot_rect, 0.1, 10, rl.Color(70, 91, 234, 255) if openpilot_selected else rl.Color(79, 79, 79, 255)) - gui_label(rl.Rectangle(openpilot_rect.x + 100, openpilot_rect.y, openpilot_rect.width - 200, radio_height), "openpilot", BODY_FONT_SIZE) - - if openpilot_selected: - checkmark_pos = rl.Vector2(openpilot_rect.x + openpilot_rect.width - 100 - self.checkmark.width, - openpilot_rect.y + radio_height / 2 - self.checkmark.height / 2) - rl.draw_texture_v(self.checkmark, checkmark_pos, rl.WHITE) + if self._software_selection_openpilot_button.selected: + self._software_selection_continue_button.set_enabled(True) + self._software_selection_custom_software_button.selected = False custom_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN * 2 + radio_height + radio_spacing, rect.width - MARGIN * 2, radio_height) - custom_selected = self.selected_radio == "custom" + self._software_selection_custom_software_button.render(custom_rect) - rl.draw_rectangle_rounded(custom_rect, 0.1, 10, rl.Color(70, 91, 234, 255) if custom_selected else rl.Color(79, 79, 79, 255)) - gui_label(rl.Rectangle(custom_rect.x + 100, custom_rect.y, custom_rect.width - 200, radio_height), "Custom Software", BODY_FONT_SIZE) - - if custom_selected: - checkmark_pos = rl.Vector2(custom_rect.x + custom_rect.width - 100 - self.checkmark.width, custom_rect.y + radio_height / 2 - self.checkmark.height / 2) - rl.draw_texture_v(self.checkmark, checkmark_pos, rl.WHITE) - - mouse_pos = rl.get_mouse_position() - if rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT): - if rl.check_collision_point_rec(mouse_pos, openpilot_rect): - self.selected_radio = "openpilot" - elif rl.check_collision_point_rec(mouse_pos, custom_rect): - self.selected_radio = "custom" + if self._software_selection_custom_software_button.selected: + self._software_selection_continue_button.set_enabled(True) + self._software_selection_openpilot_button.selected = False button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2 button_y = rect.height - BUTTON_HEIGHT - MARGIN - if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Back"): - self.state = SetupState.NETWORK_SETUP - - continue_enabled = self.selected_radio is not None - if gui_button( - rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT), - "Continue", - button_style=ButtonStyle.PRIMARY, - is_enabled=continue_enabled, - ): - if continue_enabled: - if self.selected_radio == "openpilot": - self.download(OPENPILOT_URL) - else: - self.state = SetupState.CUSTOM_URL + self._software_selection_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) + self._software_selection_continue_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT)) def render_downloading(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x, rect.y + rect.height / 2 - TITLE_FONT_SIZE / 2, rect.width, TITLE_FONT_SIZE) - gui_label(title_rect, "Downloading...", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + self._downloading_body_label.render(rl.Rectangle(rect.x, rect.y + rect.height / 2 - TITLE_FONT_SIZE / 2, rect.width, TITLE_FONT_SIZE)) def render_download_failed(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x + 117, rect.y + 185, rect.width - 117, TITLE_FONT_SIZE) - gui_label(title_rect, "Download Failed", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) + self._download_failed_title_label.render(rl.Rectangle(rect.x + 117, rect.y + 185, rect.width - 117, TITLE_FONT_SIZE)) + self._download_failed_url_label.set_text(self.failed_url) + self._download_failed_url_label.render(rl.Rectangle(rect.x + 117, rect.y + 185 + TITLE_FONT_SIZE + 67, rect.width - 117 - 100, 64)) - url_rect = rl.Rectangle(rect.x + 117, rect.y + 185 + TITLE_FONT_SIZE + 67, rect.width - 117 - 100, 64) - gui_label(url_rect, self.failed_url, 64, font_weight=FontWeight.NORMAL) - - error_rect = rl.Rectangle(rect.x + 117, rect.y + 185 + TITLE_FONT_SIZE + 67 + 64 + 48, - rect.width - 117 - 100, rect.height - 185 + TITLE_FONT_SIZE + 67 + 64 + 48 - BUTTON_HEIGHT - MARGIN * 2) - gui_text_box(error_rect, self.failed_reason, BODY_FONT_SIZE) + self._download_failed_body_label.set_text(self.failed_reason) + self._download_failed_body_label.render(rl.Rectangle(rect.x + 117, rect.y, rect.width - 117 - 100, rect.height)) button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2 button_y = rect.height - BUTTON_HEIGHT - MARGIN + self._download_failed_reboot_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) + self._download_failed_startover_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT)) - if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Reboot device"): - HARDWARE.reboot() + def render_custom_software_warning(self, rect: rl.Rectangle): + self._custom_software_warning_title_label.render(rl.Rectangle(rect.x + 50, rect.y + 150, rect.width - 265, TITLE_FONT_SIZE)) + self._custom_software_warning_body_label.render(rl.Rectangle(rect.x + 50, rect.y + 200 , rect.width - 50, BODY_FONT_SIZE * 3)) - if gui_button(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT), "Start over", - button_style=ButtonStyle.PRIMARY): - self.state = SetupState.GETTING_STARTED + button_width = (rect.width - MARGIN * 3) / 2 + button_y = rect.height - MARGIN - BUTTON_HEIGHT + self._custom_software_warning_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) + self._custom_software_warning_continue_button.render(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT)) - def render_custom_url(self): + def render_custom_software(self): def handle_keyboard_result(result): # Enter pressed if result == 1: @@ -265,6 +295,7 @@ class Setup(Widget): elif result == 0: self.state = SetupState.SOFTWARE_SELECTION + self.keyboard.reset() self.keyboard.set_title("Enter URL", "for Custom Software") gui_app.set_modal_overlay(self.keyboard, callback=handle_keyboard_result) @@ -273,7 +304,9 @@ class Setup(Widget): if re.match("^([^/.]+)/([^/]+)$", url): url = f"https://installer.comma.ai/{url}" - self.download_url = url + parsed = urlparse(url, scheme='https') + self.download_url = (urlparse(f"https://{url}") if not parsed.netloc else parsed).geturl() + self.state = SetupState.DOWNLOADING self.download_thread = threading.Thread(target=self._download_thread, daemon=True) @@ -333,7 +366,7 @@ class Setup(Widget): def main(): try: - gui_app.init_window("Setup") + gui_app.init_window("Setup", 20) setup = Setup() for _ in gui_app.render(): setup.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) diff --git a/system/ui/spinner.py b/system/ui/spinner.py index 279ddf034d..dd7fadc538 100755 --- a/system/ui/spinner.py +++ b/system/ui/spinner.py @@ -5,8 +5,8 @@ import sys from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.widget import Widget from openpilot.system.ui.text import wrap_text +from openpilot.system.ui.widgets import Widget from openpilot.system.ui.sunnypilot.lib.application import gui_app_sp diff --git a/system/ui/text.py b/system/ui/text.py index 01a580a12b..61ac043b72 100755 --- a/system/ui/text.py +++ b/system/ui/text.py @@ -3,11 +3,11 @@ import re import sys import pyray as rl from openpilot.system.hardware import HARDWARE, PC -from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle MARGIN = 50 SPACING = 40 diff --git a/system/ui/updater.py b/system/ui/updater.py index 3fd60cfb1d..31799d3628 100755 --- a/system/ui/updater.py +++ b/system/ui/updater.py @@ -7,10 +7,10 @@ from enum import IntEnum from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.label import gui_text_box, gui_label from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle +from openpilot.system.ui.widgets.label import gui_text_box, gui_label from openpilot.system.ui.widgets.network import WifiManagerUI # Constants diff --git a/system/ui/lib/widget.py b/system/ui/widgets/__init__.py similarity index 55% rename from system/ui/lib/widget.py rename to system/ui/widgets/__init__.py index 7a436b8e9f..d45f48ac38 100644 --- a/system/ui/lib/widget.py +++ b/system/ui/widgets/__init__.py @@ -2,7 +2,7 @@ import abc import pyray as rl from enum import IntEnum from collections.abc import Callable -from openpilot.system.ui.lib.application import gui_app, MousePos +from openpilot.system.ui.lib.application import gui_app, MousePos, MAX_TOUCH_SLOTS class DialogResult(IntEnum): @@ -15,29 +15,18 @@ class Widget(abc.ABC): def __init__(self): self._rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) self._parent_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) - self._is_pressed = False + self._is_pressed = [False] * MAX_TOUCH_SLOTS + # if current mouse/touch down started within the widget's rectangle + self._tracking_is_pressed = [False] * MAX_TOUCH_SLOTS + self._enabled: bool | Callable[[], bool] = True self._is_visible: bool | Callable[[], bool] = True self._touch_valid_callback: Callable[[], bool] | None = None - - def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: - """Set a callback to determine if the widget can be clicked.""" - self._touch_valid_callback = touch_callback - - def _touch_valid(self) -> bool: - """Check if the widget can be touched.""" - return self._touch_valid_callback() if self._touch_valid_callback else True - - @property - def is_visible(self) -> bool: - return self._is_visible() if callable(self._is_visible) else self._is_visible + self._multi_touch = False @property def rect(self) -> rl.Rectangle: return self._rect - def set_visible(self, visible: bool | Callable[[], bool]) -> None: - self._is_visible = visible - def set_rect(self, rect: rl.Rectangle) -> None: changed = (self._rect.x != rect.x or self._rect.y != rect.y or self._rect.width != rect.width or self._rect.height != rect.height) @@ -49,6 +38,32 @@ class Widget(abc.ABC): """Can be used like size hint in QT""" self._parent_rect = parent_rect + @property + def is_pressed(self) -> bool: + return any(self._is_pressed) + + @property + def enabled(self) -> bool: + return self._enabled() if callable(self._enabled) else self._enabled + + def set_enabled(self, enabled: bool | Callable[[], bool]) -> None: + self._enabled = enabled + + @property + def is_visible(self) -> bool: + return self._is_visible() if callable(self._is_visible) else self._is_visible + + def set_visible(self, visible: bool | Callable[[], bool]) -> None: + self._is_visible = visible + + def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: + """Set a callback to determine if the widget can be clicked.""" + self._touch_valid_callback = touch_callback + + def _touch_valid(self) -> bool: + """Check if the widget can be touched.""" + return self._touch_valid_callback() if self._touch_valid_callback else True + def set_position(self, x: float, y: float) -> None: changed = (self._rect.x != x or self._rect.y != y) self._rect.x, self._rect.y = x, y @@ -67,18 +82,37 @@ class Widget(abc.ABC): ret = self._render(self._rect) # Keep track of whether mouse down started within the widget's rectangle - for mouse_event in gui_app.mouse_events: - if mouse_event.left_pressed and self._touch_valid(): - if rl.check_collision_point_rec(mouse_event.pos, self._rect): - self._is_pressed = True + if self.enabled: + for mouse_event in gui_app.mouse_events: + if not self._multi_touch and mouse_event.slot != 0: + continue - elif not self._touch_valid(): - self._is_pressed = False + # Ignores touches/presses that start outside our rect + # Allows touch to leave the rect and come back in focus if mouse did not release + if mouse_event.left_pressed and self._touch_valid(): + if rl.check_collision_point_rec(mouse_event.pos, self._rect): + self._is_pressed[mouse_event.slot] = True + self._tracking_is_pressed[mouse_event.slot] = True - elif mouse_event.left_released: - if self._is_pressed and rl.check_collision_point_rec(mouse_event.pos, self._rect): - self._handle_mouse_release(mouse_event.pos) - self._is_pressed = False + # Callback such as scroll panel signifies user is scrolling + elif not self._touch_valid(): + self._is_pressed[mouse_event.slot] = False + self._tracking_is_pressed[mouse_event.slot] = False + + elif mouse_event.left_released: + if self._is_pressed[mouse_event.slot] and rl.check_collision_point_rec(mouse_event.pos, self._rect): + self._handle_mouse_release(mouse_event.pos) + self._is_pressed[mouse_event.slot] = False + self._tracking_is_pressed[mouse_event.slot] = False + + # Mouse/touch is still within our rect + elif rl.check_collision_point_rec(mouse_event.pos, self._rect): + if self._tracking_is_pressed[mouse_event.slot]: + self._is_pressed[mouse_event.slot] = True + + # Mouse/touch left our rect but may come back into focus later + elif not rl.check_collision_point_rec(mouse_event.pos, self._rect): + self._is_pressed[mouse_event.slot] = False return ret diff --git a/system/ui/lib/button.py b/system/ui/widgets/button.py similarity index 55% rename from system/ui/lib/button.py rename to system/ui/widgets/button.py index 121e7f86fa..41af8e51ca 100644 --- a/system/ui/lib/button.py +++ b/system/ui/widgets/button.py @@ -1,7 +1,12 @@ -import pyray as rl +from collections.abc import Callable from enum import IntEnum -from openpilot.system.ui.lib.application import gui_app, FontWeight + +import pyray as rl + +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.label import TextAlignment, Label class ButtonStyle(IntEnum): @@ -11,17 +16,15 @@ class ButtonStyle(IntEnum): TRANSPARENT = 3 # For buttons with transparent background and border ACTION = 4 LIST_ACTION = 5 # For list items with action buttons - - -class TextAlignment(IntEnum): - LEFT = 0 - CENTER = 1 - RIGHT = 2 + NO_EFFECT = 6 + KEYBOARD = 7 + FORGET_WIFI = 8 ICON_PADDING = 15 DEFAULT_BUTTON_FONT_SIZE = 60 BUTTON_DISABLED_TEXT_COLOR = rl.Color(228, 228, 228, 51) +BUTTON_DISABLED_BACKGROUND_COLOR = rl.Color(51, 51, 51, 255) ACTION_BUTTON_FONT_SIZE = 48 BUTTON_TEXT_COLOR = { @@ -31,6 +34,9 @@ BUTTON_TEXT_COLOR = { ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.ACTION: rl.Color(0, 0, 0, 255), ButtonStyle.LIST_ACTION: rl.Color(228, 228, 228, 255), + ButtonStyle.NO_EFFECT: rl.Color(228, 228, 228, 255), + ButtonStyle.KEYBOARD: rl.Color(221, 221, 221, 255), + ButtonStyle.FORGET_WIFI: rl.Color(51, 51, 51, 255), } BUTTON_BACKGROUND_COLORS = { @@ -40,6 +46,9 @@ BUTTON_BACKGROUND_COLORS = { ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.ACTION: rl.Color(189, 189, 189, 255), ButtonStyle.LIST_ACTION: rl.Color(57, 57, 57, 255), + ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255), + ButtonStyle.KEYBOARD: rl.Color(68, 68, 68, 255), + ButtonStyle.FORGET_WIFI: rl.Color(189, 189, 189, 255), } BUTTON_PRESSED_BACKGROUND_COLORS = { @@ -49,11 +58,16 @@ BUTTON_PRESSED_BACKGROUND_COLORS = { ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.ACTION: rl.Color(130, 130, 130, 255), ButtonStyle.LIST_ACTION: rl.Color(74, 74, 74, 74), + ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255), + ButtonStyle.KEYBOARD: rl.Color(51, 51, 51, 255), + ButtonStyle.FORGET_WIFI: rl.Color(130, 130, 130, 255), } _pressed_buttons: set[str] = set() # Track mouse press state globally +# TODO: This should be a Widget class + def gui_button( rect: rl.Rectangle, text: str, @@ -146,3 +160,92 @@ def gui_button( rl.draw_text_ex(font, text, text_pos, font_size, 0, color) return result + + +class Button(Widget): + def __init__(self, + text: str, + click_callback: Callable[[], None] = None, + font_size: int = DEFAULT_BUTTON_FONT_SIZE, + font_weight: FontWeight = FontWeight.MEDIUM, + button_style: ButtonStyle = ButtonStyle.NORMAL, + border_radius: int = 10, + text_alignment: TextAlignment = TextAlignment.CENTER, + text_padding: int = 20, + icon = None, + multi_touch: bool = False, + ): + + super().__init__() + self._button_style = button_style + self._border_radius = border_radius + self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style] + + self._label = Label(text, font_size, font_weight, text_alignment, text_padding, + BUTTON_TEXT_COLOR[self._button_style], icon=icon) + + self._click_callback = click_callback + self._multi_touch = multi_touch + + def set_text(self, text): + self._label.set_text(text) + + def _handle_mouse_release(self, mouse_pos: MousePos): + if self._click_callback and self.enabled: + self._click_callback() + + def _update_state(self): + if self.enabled: + self._label.set_text_color(BUTTON_TEXT_COLOR[self._button_style]) + if self.is_pressed: + self._background_color = BUTTON_PRESSED_BACKGROUND_COLORS[self._button_style] + else: + self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style] + elif self._button_style != ButtonStyle.NO_EFFECT: + self._background_color = BUTTON_DISABLED_BACKGROUND_COLOR + self._label.set_text_color(BUTTON_DISABLED_TEXT_COLOR) + + def _render(self, _): + roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2) + rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color) + self._label.render(self._rect) + + +class ButtonRadio(Button): + def __init__(self, + text: str, + icon, + click_callback: Callable[[], None] = None, + font_size: int = DEFAULT_BUTTON_FONT_SIZE, + text_alignment: TextAlignment = TextAlignment.LEFT, + border_radius: int = 10, + text_padding: int = 20, + ): + + super().__init__(text, click_callback=click_callback, font_size=font_size, + border_radius=border_radius, text_padding=text_padding, + text_alignment=text_alignment) + self._text_padding = text_padding + self._icon = icon + self.selected = False + + def _handle_mouse_release(self, mouse_pos: MousePos): + self.selected = not self.selected + if self._click_callback: + self._click_callback() + + def _update_state(self): + if self.selected: + self._background_color = BUTTON_BACKGROUND_COLORS[ButtonStyle.PRIMARY] + else: + self._background_color = BUTTON_BACKGROUND_COLORS[ButtonStyle.NORMAL] + + def _render(self, _): + roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2) + rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color) + self._label.render(self._rect) + + if self._icon and self.selected: + icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2 + icon_x = self._rect.x + self._rect.width - self._icon.width - self._text_padding - ICON_PADDING + rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE if self.enabled else rl.Color(255, 255, 255, 100)) diff --git a/system/ui/widgets/confirm_dialog.py b/system/ui/widgets/confirm_dialog.py index 00361905d6..1021b5452b 100644 --- a/system/ui/widgets/confirm_dialog.py +++ b/system/ui/widgets/confirm_dialog.py @@ -1,8 +1,9 @@ import pyray as rl from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.label import gui_text_box -from openpilot.system.ui.lib.widget import DialogResult +from openpilot.system.ui.widgets import DialogResult +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle, Button +from openpilot.system.ui.widgets.label import gui_text_box, Label +from openpilot.system.ui.widgets import Widget DIALOG_WIDTH = 1520 DIALOG_HEIGHT = 600 @@ -11,6 +12,59 @@ MARGIN = 50 TEXT_AREA_HEIGHT_REDUCTION = 200 BACKGROUND_COLOR = rl.Color(27, 27, 27, 255) +class ConfirmDialog(Widget): + def __init__(self, text: str, confirm_text: str, cancel_text: str = "Cancel"): + super().__init__() + self._label = Label(text, 70, FontWeight.BOLD) + self._cancel_button = Button(cancel_text, self._cancel_button_callback) + self._confirm_button = Button(confirm_text, self._confirm_button_callback, button_style=ButtonStyle.PRIMARY) + self._dialog_result = DialogResult.NO_ACTION + self._cancel_text = cancel_text + + def set_text(self, text): + self._label.set_text(text) + + def reset(self): + self._dialog_result = DialogResult.NO_ACTION + + def _cancel_button_callback(self): + self._dialog_result = DialogResult.CANCEL + + def _confirm_button_callback(self): + self._dialog_result = DialogResult.CONFIRM + + def _render(self, rect: rl.Rectangle): + dialog_x = (gui_app.width - DIALOG_WIDTH) / 2 + dialog_y = (gui_app.height - DIALOG_HEIGHT) / 2 + dialog_rect = rl.Rectangle(dialog_x, dialog_y, DIALOG_WIDTH, DIALOG_HEIGHT) + + bottom = dialog_rect.y + dialog_rect.height + button_width = (dialog_rect.width - 3 * MARGIN) // 2 + cancel_button_x = dialog_rect.x + MARGIN + confirm_button_x = dialog_rect.x + dialog_rect.width - button_width - MARGIN + button_y = bottom - BUTTON_HEIGHT - MARGIN + cancel_button = rl.Rectangle(cancel_button_x, button_y, button_width, BUTTON_HEIGHT) + confirm_button = rl.Rectangle(confirm_button_x, button_y, button_width, BUTTON_HEIGHT) + + rl.draw_rectangle_rec(dialog_rect, BACKGROUND_COLOR) + + text_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y, dialog_rect.width - 2 * MARGIN, dialog_rect.height - TEXT_AREA_HEIGHT_REDUCTION) + self._label.render(text_rect) + + if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER): + self._dialog_result = DialogResult.CONFIRM + elif rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE): + self._dialog_result = DialogResult.CANCEL + + if self._cancel_text: + self._confirm_button.render(confirm_button) + self._cancel_button.render(cancel_button) + else: + centered_button_x = dialog_rect.x + (dialog_rect.width - button_width) / 2 + centered_confirm_button = rl.Rectangle(centered_button_x, button_y, button_width, BUTTON_HEIGHT) + self._confirm_button.render(centered_confirm_button) + + return self._dialog_result def confirm_dialog(message: str, confirm_text: str, cancel_text: str = "Cancel") -> DialogResult: dialog_x = (gui_app.width - DIALOG_WIDTH) / 2 diff --git a/system/ui/widgets/html_render.py b/system/ui/widgets/html_render.py index 07741af456..247b7a5492 100644 --- a/system/ui/widgets/html_render.py +++ b/system/ui/widgets/html_render.py @@ -4,10 +4,10 @@ from dataclasses import dataclass from enum import Enum from typing import Any from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.widget import Widget, DialogResult -from openpilot.system.ui.lib.button import gui_button, ButtonStyle +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.widgets import Widget, DialogResult +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle class ElementType(Enum): diff --git a/system/ui/lib/inputbox.py b/system/ui/widgets/inputbox.py similarity index 85% rename from system/ui/lib/inputbox.py rename to system/ui/widgets/inputbox.py index ccf718ba56..239d63037e 100644 --- a/system/ui/lib/inputbox.py +++ b/system/ui/widgets/inputbox.py @@ -1,8 +1,8 @@ import pyray as rl import time -from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.application import gui_app, MousePos from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget PASSWORD_MASK_CHAR = "•" PASSWORD_MASK_DELAY = 1.5 # Seconds to show character before masking @@ -107,9 +107,6 @@ class InputBox(Widget): self._visible_width = rect.width self._font_size = font_size - # Handle mouse input - self._handle_mouse_input(rect, font_size) - # Draw input box rl.draw_rectangle_rec(rect, color) @@ -169,30 +166,27 @@ class InputBox(Widget): return masked_text - def _handle_mouse_input(self, rect, font_size): - """Handle mouse clicks to position cursor.""" - mouse_pos = rl.get_mouse_position() - if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) and rl.check_collision_point_rec(mouse_pos, rect): - # Calculate cursor position from click - if len(self._input_text) > 0: - font = gui_app.font() - display_text = self._get_display_text() + def _handle_mouse_release(self, mouse_pos: MousePos): + # Calculate cursor position from click + if len(self._input_text) > 0: + font = gui_app.font() + display_text = self._get_display_text() - # Find the closest character position to the click - relative_x = mouse_pos.x - (rect.x + 10) + self._text_offset - best_pos = 0 - min_distance = float('inf') + # Find the closest character position to the click + relative_x = mouse_pos.x - (self._rect.x + 10) + self._text_offset + best_pos = 0 + min_distance = float('inf') - for i in range(len(self._input_text) + 1): - char_width = measure_text_cached(font, display_text[:i], font_size).x - distance = abs(relative_x - char_width) - if distance < min_distance: - min_distance = distance - best_pos = i + for i in range(len(self._input_text) + 1): + char_width = measure_text_cached(font, display_text[:i], self._font_size).x + distance = abs(relative_x - char_width) + if distance < min_distance: + min_distance = distance + best_pos = i - self.set_cursor_position(best_pos) - else: - self.set_cursor_position(0) + self.set_cursor_position(best_pos) + else: + self.set_cursor_position(0) def _handle_keyboard_input(self): # Handle navigation keys diff --git a/system/ui/widgets/keyboard.py b/system/ui/widgets/keyboard.py index 338706e73d..25843d1ce8 100644 --- a/system/ui/widgets/keyboard.py +++ b/system/ui/widgets/keyboard.py @@ -1,11 +1,14 @@ +from functools import partial import time from typing import Literal + import pyray as rl + from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import ButtonStyle, gui_button -from openpilot.system.ui.lib.inputbox import InputBox -from openpilot.system.ui.lib.label import gui_label -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import ButtonStyle, Button +from openpilot.system.ui.widgets.inputbox import InputBox +from openpilot.system.ui.widgets.label import Label, TextAlignment KEY_FONT_SIZE = 96 DOUBLE_CLICK_THRESHOLD = 0.5 # seconds @@ -59,8 +62,8 @@ class Keyboard(Widget): self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase" self._caps_lock = False self._last_shift_press_time = 0 - self._title = "" - self._sub_title = "" + self._title = Label("", 90, FontWeight.BOLD, TextAlignment.LEFT) + self._sub_title = Label("", 55, FontWeight.NORMAL, TextAlignment.LEFT) self._max_text_size = max_text_size self._min_text_size = min_text_size @@ -73,6 +76,11 @@ class Keyboard(Widget): self._backspace_press_time: float = 0.0 self._backspace_last_repeat: float = 0.0 + self._render_return_status = -1 + self._cancel_button = Button("Cancel", self._cancel_button_callback) + + self._eye_button = Button("", self._eye_button_callback, button_style=ButtonStyle.TRANSPARENT) + self._eye_open_texture = gui_app.texture("icons/eye_open.png", 81, 54) self._eye_closed_texture = gui_app.texture("icons/eye_closed.png", 81, 54) self._key_icons = { @@ -83,6 +91,19 @@ class Keyboard(Widget): ENTER_KEY: gui_app.texture("icons/arrow-right.png", 80, 80), } + self._all_keys = {} + for l in KEYBOARD_LAYOUTS: + for _, keys in enumerate(KEYBOARD_LAYOUTS[l]): + for _, key in enumerate(keys): + if key in self._key_icons: + texture = self._key_icons[key] + self._all_keys[key] = Button("", partial(self._key_callback, key), icon=texture, + button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.KEYBOARD, multi_touch=True) + else: + self._all_keys[key] = Button(key, partial(self._key_callback, key), button_style=ButtonStyle.KEYBOARD, font_size=85, multi_touch=True) + self._all_keys[CAPS_LOCK_KEY] = Button("", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY], + button_style=ButtonStyle.KEYBOARD, multi_touch=True) + @property def text(self): return self._input_box.text @@ -94,16 +115,27 @@ class Keyboard(Widget): self._backspace_pressed = False def set_title(self, title: str, sub_title: str = ""): - self._title = title - self._sub_title = sub_title + self._title.set_text(title) + self._sub_title.set_text(sub_title) + + def _eye_button_callback(self): + self._password_mode = not self._password_mode + + def _cancel_button_callback(self): + self.clear() + self._render_return_status = 0 + + def _key_callback(self, k): + if k == ENTER_KEY: + self._render_return_status = 1 + else: + self.handle_key_press(k) def _render(self, rect: rl.Rectangle): rect = rl.Rectangle(rect.x + CONTENT_MARGIN, rect.y + CONTENT_MARGIN, rect.width - 2 * CONTENT_MARGIN, rect.height - 2 * CONTENT_MARGIN) - gui_label(rl.Rectangle(rect.x, rect.y, rect.width, 95), self._title, 90, font_weight=FontWeight.BOLD) - gui_label(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60), self._sub_title, 55, font_weight=FontWeight.NORMAL) - if gui_button(rl.Rectangle(rect.x + rect.width - 386, rect.y, 386, 125), "Cancel"): - self.clear() - return 0 + self._title.render(rl.Rectangle(rect.x, rect.y, rect.width, 95)) + self._sub_title.render(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60)) + self._cancel_button.render(rl.Rectangle(rect.x + rect.width - 386, rect.y, 386, 125)) # Draw input box and password toggle input_margin = 25 @@ -111,7 +143,7 @@ class Keyboard(Widget): self._render_input_area(input_box_rect) # Process backspace key repeat if it's held down - if not rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT): + if not self._all_keys[BACKSPACE_KEY].is_pressed: self._backspace_pressed = False if self._backspace_pressed: @@ -146,33 +178,22 @@ class Keyboard(Widget): start_x += new_width is_enabled = key != ENTER_KEY or len(self._input_box.text) >= self._min_text_size - result = -1 - # Check for backspace key press-and-hold - mouse_pos = rl.get_mouse_position() - mouse_over_key = rl.check_collision_point_rec(mouse_pos, key_rect) - - if key == BACKSPACE_KEY and mouse_over_key: - if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): - self._backspace_pressed = True - self._backspace_press_time = time.monotonic() - self._backspace_last_repeat = time.monotonic() + if key == BACKSPACE_KEY and self._all_keys[BACKSPACE_KEY].is_pressed and not self._backspace_pressed: + self._backspace_pressed = True + self._backspace_press_time = time.monotonic() + self._backspace_last_repeat = time.monotonic() if key in self._key_icons: if key == SHIFT_ACTIVE_KEY and self._caps_lock: key = CAPS_LOCK_KEY - texture = self._key_icons[key] - result = gui_button(key_rect, "", icon=texture, button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.NORMAL, is_enabled=is_enabled) + self._all_keys[key].set_enabled(is_enabled) + self._all_keys[key].render(key_rect) else: - result = gui_button(key_rect, key, KEY_FONT_SIZE, is_enabled=is_enabled) + self._all_keys[key].set_enabled(is_enabled) + self._all_keys[key].render(key_rect) - if result: - if key == ENTER_KEY: - return 1 - else: - self.handle_key_press(key) - - return -1 + return self._render_return_status def _render_input_area(self, input_rect: rl.Rectangle): if self._show_password_toggle: @@ -183,16 +204,12 @@ class Keyboard(Widget): eye_texture = self._eye_closed_texture if self._password_mode else self._eye_open_texture eye_rect = rl.Rectangle(input_rect.x + input_rect.width - 90, input_rect.y, 80, input_rect.height) + self._eye_button.render(eye_rect) + eye_x = eye_rect.x + (eye_rect.width - eye_texture.width) / 2 eye_y = eye_rect.y + (eye_rect.height - eye_texture.height) / 2 rl.draw_texture_v(eye_texture, rl.Vector2(eye_x, eye_y), rl.WHITE) - - # Handle click on eye icon - if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) and rl.check_collision_point_rec( - rl.get_mouse_position(), eye_rect - ): - self._password_mode = not self._password_mode else: self._input_box.render(input_rect) @@ -226,6 +243,10 @@ class Keyboard(Widget): if not self._caps_lock and self._layout_name == "uppercase": self._layout_name = "lowercase" + def reset(self): + self._render_return_status = -1 + self.clear() + if __name__ == "__main__": gui_app.init_window("Keyboard") diff --git a/system/ui/widgets/label.py b/system/ui/widgets/label.py new file mode 100644 index 0000000000..2da9a9f8df --- /dev/null +++ b/system/ui/widgets/label.py @@ -0,0 +1,177 @@ +from enum import IntEnum +from itertools import zip_longest + +import pyray as rl + +from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.utils import GuiStyleContext +from openpilot.system.ui.lib.emoji import find_emoji, emoji_tex +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.widgets import Widget + +ICON_PADDING = 15 + +class TextAlignment(IntEnum): + LEFT = 0 + CENTER = 1 + RIGHT = 2 + +# TODO: This should be a Widget class +def gui_label( + rect: rl.Rectangle, + text: str, + font_size: int = DEFAULT_TEXT_SIZE, + color: rl.Color = DEFAULT_TEXT_COLOR, + font_weight: FontWeight = FontWeight.NORMAL, + alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, + elide_right: bool = True +): + font = gui_app.font(font_weight) + text_size = measure_text_cached(font, text, font_size) + display_text = text + + # Elide text to fit within the rectangle + if elide_right and text_size.x > rect.width: + ellipsis = "..." + left, right = 0, len(text) + while left < right: + mid = (left + right) // 2 + candidate = text[:mid] + ellipsis + candidate_size = measure_text_cached(font, candidate, font_size) + if candidate_size.x <= rect.width: + left = mid + 1 + else: + right = mid + display_text = text[: left - 1] + ellipsis if left > 0 else ellipsis + text_size = measure_text_cached(font, display_text, font_size) + + # Calculate horizontal position based on alignment + text_x = rect.x + { + rl.GuiTextAlignment.TEXT_ALIGN_LEFT: 0, + rl.GuiTextAlignment.TEXT_ALIGN_CENTER: (rect.width - text_size.x) / 2, + rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: rect.width - text_size.x, + }.get(alignment, 0) + + # Calculate vertical position based on alignment + text_y = rect.y + { + rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: 0, + rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: (rect.height - text_size.y) / 2, + rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: rect.height - text_size.y, + }.get(alignment_vertical, 0) + + # Draw the text in the specified rectangle + rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color) + + +def gui_text_box( + rect: rl.Rectangle, + text: str, + font_size: int = DEFAULT_TEXT_SIZE, + color: rl.Color = DEFAULT_TEXT_COLOR, + alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, + font_weight: FontWeight = FontWeight.NORMAL, +): + styles = [ + (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(color)), + (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, font_size), + (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_LINE_SPACING, font_size), + (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_ALIGNMENT, alignment), + (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_ALIGNMENT_VERTICAL, alignment_vertical), + (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_WRAP_MODE, rl.GuiTextWrapMode.TEXT_WRAP_WORD) + ] + if font_weight != FontWeight.NORMAL: + rl.gui_set_font(gui_app.font(font_weight)) + + with GuiStyleContext(styles): + rl.gui_label(rect, text) + + if font_weight != FontWeight.NORMAL: + rl.gui_set_font(gui_app.font(FontWeight.NORMAL)) + + +# Non-interactive text area. Can render emojis and an optional specified icon. +class Label(Widget): + def __init__(self, + text: str, + font_size: int = DEFAULT_TEXT_SIZE, + font_weight: FontWeight = FontWeight.NORMAL, + text_alignment: TextAlignment = TextAlignment.CENTER, + text_padding: int = 20, + text_color: rl.Color = DEFAULT_TEXT_COLOR, + icon = None, + ): + + super().__init__() + self._font_weight = font_weight + self._font = gui_app.font(self._font_weight) + self._font_size = font_size + self._text_alignment = text_alignment + self._text_padding = text_padding + self._text_color = text_color + self._icon = icon + self.set_text(text) + + def set_text(self, text): + self._text_raw = text + self._update_text(self._text_raw) + + def set_text_color(self, color): + self._text_color = color + + def _update_layout_rects(self): + self._update_text(self._text_raw) + + def _update_text(self, text): + self._emojis = [] + self._text_size = [] + self._text = wrap_text(self._font, text, self._font_size, self._rect.width - (self._text_padding*2)) + for t in self._text: + self._emojis.append(find_emoji(t)) + self._text_size.append(measure_text_cached(self._font, t, self._font_size)) + + def _render(self, _): + text = self._text[0] if self._text else None + text_size = self._text_size[0] if self._text_size else rl.Vector2(0.0, 0.0) + text_pos = rl.Vector2(0, (self._rect.y + (self._rect.height - (text_size.y)) // 2)) + + if self._icon: + icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2 + if text: + if self._text_alignment == TextAlignment.LEFT: + icon_x = self._rect.x + self._text_padding + text_pos.x = self._icon.width + ICON_PADDING + elif self._text_alignment == TextAlignment.CENTER: + total_width = self._icon.width + ICON_PADDING + text_size.x + icon_x = self._rect.x + (self._rect.width - total_width) / 2 + text_pos.x = self._icon.width + ICON_PADDING + else: + icon_x = (self._rect.x + self._rect.width - text_size.x - self._text_padding) - ICON_PADDING - self._icon.width + else: + icon_x = self._rect.x + (self._rect.width - self._icon.width) / 2 + rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE) + + for text, text_size, emojis in zip_longest(self._text, self._text_size, self._emojis, fillvalue=[]): + line_pos = rl.Vector2(text_pos.x, text_pos.y) + if self._text_alignment == TextAlignment.LEFT: + line_pos.x += self._rect.x + self._text_padding + elif self._text_alignment == TextAlignment.CENTER: + line_pos.x += self._rect.x + (self._rect.width - text_size.x) // 2 + elif self._text_alignment == TextAlignment.RIGHT: + line_pos.x += self._rect.x + self._rect.width - text_size.x - self._text_padding + + prev_index = 0 + for start, end, emoji in emojis: + text_before = text[prev_index:start] + width_before = measure_text_cached(self._font, text_before, self._font_size) + rl.draw_text_ex(self._font, text_before, line_pos, self._font_size, 0, self._text_color) + line_pos.x += width_before.x + + tex = emoji_tex(emoji) + rl.draw_texture_ex(tex, line_pos, 0.0, self._font_size / tex.height, self._text_color) + line_pos.x += self._font_size + prev_index = end + rl.draw_text_ex(self._font, text[prev_index:], line_pos, self._font_size, 0, self._text_color) + text_pos.y += text_size.y or self._font_size diff --git a/system/ui/lib/list_view.py b/system/ui/widgets/list_view.py similarity index 97% rename from system/ui/lib/list_view.py rename to system/ui/widgets/list_view.py index 4b6f434de3..e871eef0a1 100644 --- a/system/ui/lib/list_view.py +++ b/system/ui/widgets/list_view.py @@ -5,9 +5,9 @@ from abc import ABC from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.wrap_text import wrap_text -from openpilot.system.ui.lib.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle +from openpilot.system.ui.widgets.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT ITEM_BASE_WIDTH = 600 ITEM_BASE_HEIGHT = 170 @@ -167,7 +167,7 @@ class MultipleButtonAction(ItemAction): # Check button state mouse_pos = rl.get_mouse_position() is_hovered = rl.check_collision_point_rec(mouse_pos, button_rect) - is_pressed = is_hovered and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed + is_pressed = is_hovered and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed is_selected = i == self.selected_button # Button colors @@ -188,7 +188,7 @@ class MultipleButtonAction(ItemAction): rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, rl.Color(228, 228, 228, 255)) # Handle click - if is_hovered and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed: + if is_hovered and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed: clicked = i if clicked >= 0: diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 0613bb1f12..4eac1214c2 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -1,16 +1,17 @@ from dataclasses import dataclass +from functools import partial from threading import Lock from typing import Literal import pyray as rl from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.button import ButtonStyle, gui_button -from openpilot.system.ui.lib.label import gui_label from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.wifi_manager import NetworkInfo, WifiManagerCallbacks, WifiManagerWrapper, SecurityType +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import ButtonStyle, Button, TextAlignment +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.keyboard import Keyboard -from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets.label import gui_label NM_DEVICE_STATE_NEED_AUTH = 60 MIN_PASSWORD_LENGTH = 8 @@ -40,6 +41,7 @@ class StateConnecting: @dataclass class StateNeedsAuth: network: NetworkInfo + retry: bool action: Literal["needs_auth"] = "needs_auth" @@ -67,8 +69,11 @@ class WifiManagerUI(Widget): self.keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True) self._networks: list[NetworkInfo] = [] + self._networks_buttons: dict[str, Button] = {} + self._forget_networks_buttons: dict[str, Button] = {} self._lock = Lock() self.wifi_manager = wifi_manager + self._confirm_dialog = ConfirmDialog("", "Forget", "Cancel") self.wifi_manager.set_callbacks( WifiManagerCallbacks( @@ -89,12 +94,14 @@ class WifiManagerUI(Widget): return match self.state: - case StateNeedsAuth(network): - self.keyboard.set_title("Enter password", f"for {network.ssid}") + case StateNeedsAuth(network, retry): + self.keyboard.set_title("Wrong password" if retry else "Enter password", f"for {network.ssid}") + self.keyboard.reset() gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(network, result)) case StateShowForgetConfirm(network): - gui_app.set_modal_overlay(lambda: confirm_dialog(f'Forget Wi-Fi Network "{network.ssid}"?', "Forget"), - callback=lambda result: self.on_forgot_confirm_finished(network, result)) + self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{network.ssid}"?') + self._confirm_dialog.reset() + gui_app.set_modal_overlay(self._confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(network, result)) case _: self._draw_network_list(rect) @@ -139,16 +146,20 @@ class WifiManagerUI(Widget): signal_icon_rect = rl.Rectangle(rect.x + rect.width - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE) security_icon_rect = rl.Rectangle(signal_icon_rect.x - spacing - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE) - gui_label(ssid_rect, network.ssid, 55) - status_text = "" match self.state: case StateConnecting(network=connecting): if connecting.ssid == network.ssid: + self._networks_buttons[network.ssid].set_enabled(False) status_text = "CONNECTING..." case StateForgetting(network=forgetting): if forgetting.ssid == network.ssid: + self._networks_buttons[network.ssid].set_enabled(False) status_text = "FORGETTING..." + case _: + self._networks_buttons[network.ssid].set_enabled(True) + + self._networks_buttons[network.ssid].render(ssid_rect) if status_text: status_text_rect = rl.Rectangle(security_icon_rect.x - 410, rect.y, 410, ITEM_HEIGHT) @@ -162,18 +173,22 @@ class WifiManagerUI(Widget): self.btn_width, 80, ) - if isinstance(self.state, StateIdle) and gui_button(forget_btn_rect, "Forget", button_style=ButtonStyle.ACTION) and clicked: - self.state = StateShowForgetConfirm(network) + self._forget_networks_buttons[network.ssid].render(forget_btn_rect) self._draw_status_icon(security_icon_rect, network) self._draw_signal_strength_icon(signal_icon_rect, network) - if isinstance(self.state, StateIdle) and rl.check_collision_point_rec(rl.get_mouse_position(), ssid_rect) and clicked: + def _networks_buttons_callback(self, network): + if self.scroll_panel.is_touch_valid(): if not network.is_saved and network.security_type != SecurityType.OPEN: - self.state = StateNeedsAuth(network) + self.state = StateNeedsAuth(network, False) elif not network.is_connected: self.connect_to_network(network) + def _forget_networks_buttons_callback(self, network): + if self.scroll_panel.is_touch_valid(): + self.state = StateShowForgetConfirm(network) + def _draw_status_icon(self, rect, network: NetworkInfo): """Draw the status icon based on network's connection state""" icon_file = None @@ -211,12 +226,17 @@ class WifiManagerUI(Widget): def _on_network_updated(self, networks: list[NetworkInfo]): with self._lock: self._networks = networks + for n in self._networks: + self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55, text_alignment=TextAlignment.LEFT, + button_style=ButtonStyle.NO_EFFECT) + self._forget_networks_buttons[n.ssid] = Button("Forget", partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, + font_size=45) def _on_need_auth(self, ssid): with self._lock: network = next((n for n in self._networks if n.ssid == ssid), None) if network: - self.state = StateNeedsAuth(network) + self.state = StateNeedsAuth(network, True) def _on_activated(self): with self._lock: diff --git a/system/ui/widgets/option_dialog.py b/system/ui/widgets/option_dialog.py index a2958ee1f4..3140f419f5 100644 --- a/system/ui/widgets/option_dialog.py +++ b/system/ui/widgets/option_dialog.py @@ -1,9 +1,9 @@ import pyray as rl from openpilot.system.ui.lib.application import FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle, TextAlignment -from openpilot.system.ui.lib.label import gui_label from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle, TextAlignment +from openpilot.system.ui.widgets.label import gui_label # Constants MARGIN = 50 diff --git a/system/ui/lib/scroller.py b/system/ui/widgets/scroller.py similarity index 97% rename from system/ui/lib/scroller.py rename to system/ui/widgets/scroller.py index 8a10221772..f7304bf6a6 100644 --- a/system/ui/lib/scroller.py +++ b/system/ui/widgets/scroller.py @@ -1,6 +1,6 @@ import pyray as rl -from openpilot.system.ui.lib.widget import Widget from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel +from openpilot.system.ui.widgets import Widget ITEM_SPACING = 40 LINE_COLOR = rl.GRAY diff --git a/system/ui/lib/toggle.py b/system/ui/widgets/toggle.py similarity index 94% rename from system/ui/lib/toggle.py rename to system/ui/widgets/toggle.py index 2edf2ff11c..eba5ef9e58 100644 --- a/system/ui/lib/toggle.py +++ b/system/ui/widgets/toggle.py @@ -1,6 +1,6 @@ import pyray as rl from openpilot.system.ui.lib.application import MousePos -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget ON_COLOR = rl.Color(51, 171, 76, 255) OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255) @@ -38,9 +38,6 @@ class Toggle(Widget): self._state = state self._target = 1.0 if state else 0.0 - def set_enabled(self, enabled: bool): - self._enabled = enabled - def is_enabled(self): return self._enabled diff --git a/system/updated/tests/test_base.py b/system/updated/tests/test_base.py index 52287c58f9..699a0f0bd3 100644 --- a/system/updated/tests/test_base.py +++ b/system/updated/tests/test_base.py @@ -132,8 +132,8 @@ class TestBaseUpdate: class ParamsBaseUpdateTest(TestBaseUpdate): def _test_finalized_update(self, branch, version, agnos_version, release_notes): - assert self.params.get("UpdaterNewDescription", encoding="utf-8").startswith(f"{version} / {branch}") - assert self.params.get("UpdaterNewReleaseNotes", encoding="utf-8") == f"{release_notes}\n" + assert self.params.get("UpdaterNewDescription").startswith(f"{version} / {branch}") + assert self.params.get("UpdaterNewReleaseNotes") == f"{release_notes}\n" super()._test_finalized_update(branch, version, agnos_version, release_notes) def send_check_for_updates_signal(self, updated: ManagerProcess): @@ -143,16 +143,16 @@ class ParamsBaseUpdateTest(TestBaseUpdate): updated.signal(signal.SIGHUP.value) def _test_params(self, branch, fetch_available, update_available): - assert self.params.get("UpdaterTargetBranch", encoding="utf-8") == branch + assert self.params.get("UpdaterTargetBranch") == branch assert self.params.get_bool("UpdaterFetchAvailable") == fetch_available assert self.params.get_bool("UpdateAvailable") == update_available def wait_for_idle(self): - self.wait_for_condition(lambda: self.params.get("UpdaterState", encoding="utf-8") == "idle") + self.wait_for_condition(lambda: self.params.get("UpdaterState") == "idle") def wait_for_failed(self): - self.wait_for_condition(lambda: self.params.get("UpdateFailedCount", encoding="utf-8") is not None and \ - int(self.params.get("UpdateFailedCount", encoding="utf-8")) > 0) + self.wait_for_condition(lambda: self.params.get("UpdateFailedCount") is not None and \ + self.params.get("UpdateFailedCount") > 0) def wait_for_fetch_available(self): self.wait_for_condition(lambda: self.params.get_bool("UpdaterFetchAvailable")) diff --git a/system/updated/updated.py b/system/updated/updated.py index 0759c0a7aa..1fd9e8b717 100755 --- a/system/updated/updated.py +++ b/system/updated/updated.py @@ -31,8 +31,13 @@ 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 +# do not allow to engage after this many hours onroad and this many routes +HOURS_NO_CONNECTIVITY_MAX = 27 +ROUTES_NO_CONNECTIVITY_MAX = 84 +# send an offroad prompt after this many hours onroad and this many routes +HOURS_NO_CONNECTIVITY_PROMPT = 23 +ROUTES_NO_CONNECTIVITY_PROMPT = 80 + class UserRequest: NONE = 0 @@ -61,15 +66,7 @@ class WaitTimeHelper: def write_time_to_param(params, param) -> None: t = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - params.put(param, t.isoformat().encode('utf8')) - -def read_time_from_param(params, param) -> datetime.datetime | None: - t = params.get(param, encoding='utf8') - try: - return datetime.datetime.fromisoformat(t) - except (TypeError, ValueError): - pass - return None + params.put(param, t) def run(cmd: list[str], cwd: str = None) -> str: return subprocess.check_output(cmd, cwd=cwd, stderr=subprocess.STDOUT, encoding='utf8') @@ -242,7 +239,7 @@ class Updater: @property def target_branch(self) -> str: - b: str | None = self.params.get("UpdaterTargetBranch", encoding='utf-8') + b: str | None = self.params.get("UpdaterTargetBranch") if b is None: b = self.get_branch(BASEDIR) return b @@ -272,20 +269,22 @@ class Updater: return run(["git", "rev-parse", "HEAD"], path).rstrip() def set_params(self, update_success: bool, failed_count: int, exception: str | None) -> None: - self.params.put("UpdateFailedCount", str(failed_count)) + self.params.put("UpdateFailedCount", failed_count) self.params.put("UpdaterTargetBranch", self.target_branch) self.params.put_bool("UpdaterFetchAvailable", self.update_available) if len(self.branches): self.params.put("UpdaterAvailableBranches", ','.join(self.branches.keys())) - last_update = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + last_uptime_onroad = self.params.get("UptimeOnroad", return_default=True) + last_route_count = self.params.get("RouteCount", return_default=True) if update_success: - write_time_to_param(self.params, "LastUpdateTime") + self.params.put("LastUpdateTime", datetime.datetime.now(datetime.UTC).replace(tzinfo=None)) + self.params.put("LastUpdateUptimeOnroad", last_uptime_onroad) + self.params.put("LastUpdateRouteCount", last_route_count) else: - t = read_time_from_param(self.params, "LastUpdateTime") - if t is not None: - last_update = t + last_uptime_onroad = self.params.get("LastUpdateUptimeOnroad") or last_uptime_onroad + last_route_count = self.params.get("LastUpdateRouteCount") or last_route_count if exception is None: self.params.remove("LastUpdateException") @@ -323,8 +322,8 @@ class Updater: for alert in ("Offroad_UpdateFailed", "Offroad_ConnectivityNeeded", "Offroad_ConnectivityNeededPrompt"): set_offroad_alert(alert, False) - now = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - dt = now - last_update + dt_uptime_onroad = (self.params.get("UptimeOnroad", return_default=True) - last_uptime_onroad) / (60*60) + dt_route_count = self.params.get("RouteCount", return_default=True) - last_route_count build_metadata = get_build_metadata() if failed_count > 15 and exception is not None and self.has_internet: if build_metadata.tested_channel: @@ -333,11 +332,11 @@ class Updater: extra_text = exception set_offroad_alert("Offroad_UpdateFailed", True, extra_text=extra_text) elif failed_count > 0: - if dt.days > DAYS_NO_CONNECTIVITY_MAX: + if dt_uptime_onroad > HOURS_NO_CONNECTIVITY_MAX and dt_route_count > ROUTES_NO_CONNECTIVITY_MAX: set_offroad_alert("Offroad_ConnectivityNeeded", True) - elif dt.days > DAYS_NO_CONNECTIVITY_PROMPT: - remaining = max(DAYS_NO_CONNECTIVITY_MAX - dt.days, 1) - set_offroad_alert("Offroad_ConnectivityNeededPrompt", True, extra_text=f"{remaining} day{'' if remaining == 1 else 's'}.") + elif dt_uptime_onroad > HOURS_NO_CONNECTIVITY_PROMPT and dt_route_count > ROUTES_NO_CONNECTIVITY_PROMPT: + remaining = max(HOURS_NO_CONNECTIVITY_MAX - dt_uptime_onroad, 1) + set_offroad_alert("Offroad_ConnectivityNeededPrompt", True, extra_text=f"{remaining} hour{'' if remaining == 1 else 's'}.") def check_for_update(self) -> None: cloudlog.info("checking for updates") @@ -429,8 +428,8 @@ def main() -> None: cloudlog.event("update installed") if not params.get("InstallDate"): - t = datetime.datetime.now(datetime.UTC).replace(tzinfo=None).isoformat() - params.put("InstallDate", t.encode('utf8')) + t = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + params.put("InstallDate", t) updater = Updater() update_failed_count = 0 # TODO: Load from param? @@ -468,7 +467,7 @@ def main() -> None: updater.check_for_update() # download update - last_fetch = read_time_from_param(params, "UpdaterLastFetchTime") + last_fetch = params.get("UpdaterLastFetchTime") timed_out = last_fetch is None or (datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - last_fetch > datetime.timedelta(days=3)) user_requested_fetch = wait_helper.user_request == UserRequest.FETCH if params.get_bool("NetworkMetered") and not timed_out and not user_requested_fetch: diff --git a/system/version.py b/system/version.py index 34c9ade24d..9bf8855a28 100755 --- a/system/version.py +++ b/system/version.py @@ -18,8 +18,8 @@ TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] + BUILD_METADATA_FILENAME = "build.json" -training_version: bytes = b"0.2.0" -terms_version: bytes = b"2" +training_version: str = "0.2.0" +terms_version: str = "2" def get_version(path: str = BASEDIR) -> str: diff --git a/third_party/raylib/build.sh b/third_party/raylib/build.sh index 9e85f90df9..544feb1c59 100755 --- a/third_party/raylib/build.sh +++ b/third_party/raylib/build.sh @@ -30,7 +30,7 @@ fi cd raylib_repo -COMMIT=${1:-66030a7de62c9e1ee8ab30a1d657a740333bb4f2} +COMMIT=${1:-39e6d8b52db159ba2ab3214b46d89a8069e09394} git fetch origin $COMMIT git reset --hard $COMMIT git clean -xdff . diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index c8e6093b86..89be3cceb2 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -15,7 +15,8 @@ else: qt_libs = ['qt_util'] + base_libs cabana_env = qt_env.Clone() -cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, 'panda', 'avutil', 'avcodec', 'avformat', 'bz2', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs + +cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc/dbc").abspath) cabana_env['CXXFLAGS'] += [opendbc_path] @@ -29,7 +30,8 @@ cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/socketcans 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'utils/export.cc', 'utils/util.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', - 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) + 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', + 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) cabana_env.Program('cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) if GetOption('extras'): @@ -39,4 +41,4 @@ output_json_file = 'tools/cabana/dbc/car_fingerprint_to_dbc.json' generate_dbc = cabana_env.Command('#' + output_json_file, ['dbc/generate_dbc_json.py'], "python3 tools/cabana/dbc/generate_dbc_json.py --out " + output_json_file) -cabana_env.Depends(generate_dbc, ["#common", "#selfdrive/pandad", '#opendbc_repo', "#cereal", "#msgq_repo"]) +cabana_env.Depends(generate_dbc, ["#common", '#opendbc_repo', "#cereal", "#msgq_repo"]) diff --git a/tools/cabana/cameraview.cc b/tools/cabana/cameraview.cc new file mode 100644 index 0000000000..13c838efd8 --- /dev/null +++ b/tools/cabana/cameraview.cc @@ -0,0 +1,261 @@ +#include "tools/cabana/cameraview.h" + +#ifdef __APPLE__ +#include +#else +#include +#endif + +#include + +namespace { + +const char frame_vertex_shader[] = +#ifdef __APPLE__ + "#version 330 core\n" +#else + "#version 300 es\n" +#endif + "layout(location = 0) in vec4 aPosition;\n" + "layout(location = 1) in vec2 aTexCoord;\n" + "uniform mat4 uTransform;\n" + "out vec2 vTexCoord;\n" + "void main() {\n" + " gl_Position = uTransform * aPosition;\n" + " vTexCoord = aTexCoord;\n" + "}\n"; + +const char frame_fragment_shader[] = +#ifdef __APPLE__ + "#version 330 core\n" +#else + "#version 300 es\n" + "precision mediump float;\n" +#endif + "uniform sampler2D uTextureY;\n" + "uniform sampler2D uTextureUV;\n" + "in vec2 vTexCoord;\n" + "out vec4 colorOut;\n" + "void main() {\n" + " float y = texture(uTextureY, vTexCoord).r;\n" + " vec2 uv = texture(uTextureUV, vTexCoord).rg - 0.5;\n" + " float r = y + 1.402 * uv.y;\n" + " float g = y - 0.344 * uv.x - 0.714 * uv.y;\n" + " float b = y + 1.772 * uv.x;\n" + " colorOut = vec4(r, g, b, 1.0);\n" + "}\n"; + +} // namespace + +CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, QWidget* parent) : + stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QOpenGLWidget(parent) { + setAttribute(Qt::WA_OpaquePaintEvent); + qRegisterMetaType>("availableStreams"); + QObject::connect(this, &CameraWidget::vipcThreadConnected, this, &CameraWidget::vipcConnected, Qt::BlockingQueuedConnection); + QObject::connect(this, &CameraWidget::vipcThreadFrameReceived, this, &CameraWidget::vipcFrameReceived, Qt::QueuedConnection); + QObject::connect(this, &CameraWidget::vipcAvailableStreamsUpdated, this, &CameraWidget::availableStreamsUpdated, Qt::QueuedConnection); + QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit, this, &CameraWidget::stopVipcThread); +} + +CameraWidget::~CameraWidget() { + makeCurrent(); + stopVipcThread(); + if (isValid()) { + glDeleteVertexArrays(1, &frame_vao); + glDeleteBuffers(1, &frame_vbo); + glDeleteBuffers(1, &frame_ibo); + glDeleteTextures(2, textures); + shader_program_.reset(); + } + doneCurrent(); +} + +void CameraWidget::initializeGL() { + initializeOpenGLFunctions(); + + shader_program_ = std::make_unique(context()); + shader_program_->addShaderFromSourceCode(QOpenGLShader::Vertex, frame_vertex_shader); + shader_program_->addShaderFromSourceCode(QOpenGLShader::Fragment, frame_fragment_shader); + shader_program_->link(); + + GLint frame_pos_loc = shader_program_->attributeLocation("aPosition"); + GLint frame_texcoord_loc = shader_program_->attributeLocation("aTexCoord"); + + auto [x1, x2, y1, y2] = requested_stream_type == VISION_STREAM_DRIVER ? std::tuple(0.f, 1.f, 1.f, 0.f) : std::tuple(1.f, 0.f, 1.f, 0.f); + const uint8_t frame_indicies[] = {0, 1, 2, 0, 2, 3}; + const float frame_coords[4][4] = { + {-1.0, -1.0, x2, y1}, // bl + {-1.0, 1.0, x2, y2}, // tl + { 1.0, 1.0, x1, y2}, // tr + { 1.0, -1.0, x1, y1}, // br + }; + + glGenVertexArrays(1, &frame_vao); + glBindVertexArray(frame_vao); + glGenBuffers(1, &frame_vbo); + glBindBuffer(GL_ARRAY_BUFFER, frame_vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(frame_coords), frame_coords, GL_STATIC_DRAW); + glEnableVertexAttribArray(frame_pos_loc); + glVertexAttribPointer(frame_pos_loc, 2, GL_FLOAT, GL_FALSE, + sizeof(frame_coords[0]), (const void *)0); + glEnableVertexAttribArray(frame_texcoord_loc); + glVertexAttribPointer(frame_texcoord_loc, 2, GL_FLOAT, GL_FALSE, + sizeof(frame_coords[0]), (const void *)(sizeof(float) * 2)); + glGenBuffers(1, &frame_ibo); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, frame_ibo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(frame_indicies), frame_indicies, GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + glGenTextures(2, textures); + + shader_program_->bind(); + shader_program_->setUniformValue("uTextureY", 0); + shader_program_->setUniformValue("uTextureUV", 1); + shader_program_->release(); +} + +void CameraWidget::showEvent(QShowEvent *event) { + if (!vipc_thread) { + clearFrames(); + vipc_thread = new QThread(); + connect(vipc_thread, &QThread::started, [=]() { vipcThread(); }); + connect(vipc_thread, &QThread::finished, vipc_thread, &QObject::deleteLater); + vipc_thread->start(); + } +} + +void CameraWidget::stopVipcThread() { + makeCurrent(); + if (vipc_thread) { + vipc_thread->requestInterruption(); + vipc_thread->quit(); + vipc_thread->wait(); + vipc_thread = nullptr; + } +} + +void CameraWidget::availableStreamsUpdated(std::set streams) { + available_streams = streams; +} + +void CameraWidget::paintGL() { + glClearColor(bg.redF(), bg.greenF(), bg.blueF(), bg.alphaF()); + glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + + std::lock_guard lk(frame_lock); + if (!current_frame_) return; + + // Scale for aspect ratio + float widget_ratio = (float)width() / height(); + float frame_ratio = (float)stream_width / stream_height; + float scale_x = std::min(frame_ratio / widget_ratio, 1.0f); + float scale_y = std::min(widget_ratio / frame_ratio, 1.0f); + + glViewport(0, 0, width() * devicePixelRatio(), height() * devicePixelRatio()); + + shader_program_->bind(); + QMatrix4x4 transform; + transform.scale(scale_x, scale_y, 1.0f); + shader_program_->setUniformValue("uTransform", transform); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, textures[0]); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width, stream_height, GL_RED, GL_UNSIGNED_BYTE, current_frame_->y); + + glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride/2); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, textures[1]); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width/2, stream_height/2, GL_RG, GL_UNSIGNED_BYTE, current_frame_->uv); + + glBindVertexArray(frame_vao); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, nullptr); + glBindVertexArray(0); + + // Reset both texture units + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, 0); + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + + shader_program_->release(); +} + +void CameraWidget::vipcConnected(VisionIpcClient *vipc_client) { + makeCurrent(); + stream_width = vipc_client->buffers[0].width; + stream_height = vipc_client->buffers[0].height; + stream_stride = vipc_client->buffers[0].stride; + + glBindTexture(GL_TEXTURE_2D, textures[0]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, stream_width, stream_height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + assert(glGetError() == GL_NO_ERROR); + + glBindTexture(GL_TEXTURE_2D, textures[1]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, stream_width/2, stream_height/2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr); + assert(glGetError() == GL_NO_ERROR); +} + +void CameraWidget::vipcFrameReceived() { + update(); +} + +void CameraWidget::vipcThread() { + VisionStreamType cur_stream = requested_stream_type; + std::unique_ptr vipc_client; + VisionIpcBufExtra frame_meta = {}; + + while (!QThread::currentThread()->isInterruptionRequested()) { + if (!vipc_client || cur_stream != requested_stream_type) { + clearFrames(); + qDebug().nospace() << "connecting to stream " << requested_stream_type << ", was connected to " << cur_stream; + cur_stream = requested_stream_type; + vipc_client.reset(new VisionIpcClient(stream_name, cur_stream, false)); + } + active_stream_type = cur_stream; + + if (!vipc_client->connected) { + clearFrames(); + auto streams = VisionIpcClient::getAvailableStreams(stream_name, false); + if (streams.empty()) { + QThread::msleep(100); + continue; + } + emit vipcAvailableStreamsUpdated(streams); + + if (!vipc_client->connect(false)) { + QThread::msleep(100); + continue; + } + emit vipcThreadConnected(vipc_client.get()); + } + + if (VisionBuf *buf = vipc_client->recv(&frame_meta, 100)) { + { + std::lock_guard lk(frame_lock); + current_frame_ = buf; + frame_meta_ = frame_meta; + } + emit vipcThreadFrameReceived(); + } + } +} + +void CameraWidget::clearFrames() { + std::lock_guard lk(frame_lock); + current_frame_ = nullptr; + available_streams.clear(); +} diff --git a/tools/cabana/cameraview.h b/tools/cabana/cameraview.h new file mode 100644 index 0000000000..930b13d82c --- /dev/null +++ b/tools/cabana/cameraview.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "msgq/visionipc/visionipc_client.h" + +class CameraWidget : public QOpenGLWidget, protected QOpenGLFunctions { + Q_OBJECT + +public: + using QOpenGLWidget::QOpenGLWidget; + explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, QWidget* parent = nullptr); + ~CameraWidget(); + void setStreamType(VisionStreamType type) { requested_stream_type = type; } + VisionStreamType getStreamType() { return active_stream_type; } + void stopVipcThread(); + +signals: + void clicked(); + void vipcThreadConnected(VisionIpcClient *); + void vipcThreadFrameReceived(); + void vipcAvailableStreamsUpdated(std::set); + +protected: + void paintGL() override; + void initializeGL() override; + void showEvent(QShowEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); } + void vipcThread(); + void clearFrames(); + + GLuint frame_vao, frame_vbo, frame_ibo; + GLuint textures[2]; + std::unique_ptr shader_program_; + QColor bg = Qt::black; + + std::string stream_name; + int stream_width = 0; + int stream_height = 0; + int stream_stride = 0; + std::atomic active_stream_type; + std::atomic requested_stream_type; + std::set available_streams; + QThread *vipc_thread = nullptr; + std::recursive_mutex frame_lock; + VisionBuf* current_frame_ = nullptr; + VisionIpcBufExtra frame_meta_ = {}; + +protected slots: + void vipcConnected(VisionIpcClient *vipc_client); + void vipcFrameReceived(); + void availableStreamsUpdated(std::set streams); +}; + +Q_DECLARE_METATYPE(std::set); diff --git a/tools/cabana/chart/chart.cc b/tools/cabana/chart/chart.cc index 3588c5edc6..505b2b8053 100644 --- a/tools/cabana/chart/chart.cc +++ b/tools/cabana/chart/chart.cc @@ -45,7 +45,7 @@ ChartView::ChartView(const std::pair &x_range, ChartsWidget *par createToolButtons(); setRubberBand(QChartView::HorizontalRubberBand); setMouseTracking(true); - setTheme(settings.theme == DARK_THEME ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight); + setTheme(utils::isDarkTheme() ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight); signal_value_font.setPointSize(9); QObject::connect(axis_y, &QValueAxis::rangeChanged, this, &ChartView::resetChartCache); @@ -747,7 +747,7 @@ void ChartView::drawTimeline(QPainter *painter) { QRectF time_str_rect(QPointF(x - time_str_size.width() / 2.0, plot_area.bottom() + AXIS_X_TOP_MARGIN), time_str_size); QPainterPath path; path.addRoundedRect(time_str_rect, 3, 3); - painter->fillPath(path, settings.theme == DARK_THEME ? Qt::darkGray : Qt::gray); + painter->fillPath(path, utils::isDarkTheme() ? Qt::darkGray : Qt::gray); painter->setPen(palette().color(QPalette::BrightText)); painter->setFont(axis_x->labelsFont()); painter->drawText(time_str_rect, Qt::AlignCenter, time_str); diff --git a/tools/cabana/chart/chartswidget.cc b/tools/cabana/chart/chartswidget.cc index 08840f4663..3e9e452b90 100644 --- a/tools/cabana/chart/chartswidget.cc +++ b/tools/cabana/chart/chartswidget.cc @@ -254,7 +254,7 @@ void ChartsWidget::settingChanged() { if (std::exchange(current_theme, settings.theme) != current_theme) { undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise")); redo_zoom_action->setIcon(utils::icon("arrow-clockwise")); - auto theme = settings.theme == DARK_THEME ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight; + auto theme = utils::isDarkTheme() ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight; for (auto c : charts) { c->setTheme(theme); } diff --git a/tools/cabana/chart/sparkline.cc b/tools/cabana/chart/sparkline.cc index 09f86a095a..91435cd5ac 100644 --- a/tools/cabana/chart/sparkline.cc +++ b/tools/cabana/chart/sparkline.cc @@ -4,51 +4,97 @@ #include #include -void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size) { - points.clear(); - double value = 0; - auto [first, last] = can->eventsInRange(msg_id, std::make_pair(last_msg_ts -range, last_msg_ts)); - for (auto it = first; it != last; ++it) { - if (sig->getValue((*it)->dat, (*it)->size, &value)) { - points.emplace_back(((*it)->mono_time - (*first)->mono_time) / 1e9, value); - } - } - - if (points.empty() || size.isEmpty()) { +void Sparkline::update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, QSize size) { + if (first == last || size.isEmpty()) { pixmap = QPixmap(); return; } - const auto [min, max] = std::minmax_element(points.begin(), points.end(), - [](auto &l, auto &r) { return l.y() < r.y(); }); - min_val = min->y() == max->y() ? min->y() - 1 : min->y(); - max_val = min->y() == max->y() ? max->y() + 1 : max->y(); - freq_ = points.size() / std::max(points.back().x() - points.front().x(), 1.0); + points_.clear(); + min_val = std::numeric_limits::max(); + max_val = std::numeric_limits::lowest(); + points_.reserve(std::distance(first, last)); + + uint64_t start_time = (*first)->mono_time; + double value = 0.0; + for (auto it = first; it != last; ++it) { + if (sig->getValue((*it)->dat, (*it)->size, &value)) { + min_val = std::min(min_val, value); + max_val = std::max(max_val, value); + points_.emplace_back(((*it)->mono_time - start_time) / 1e9, value); + } + } + + if (points_.empty()) { + pixmap = QPixmap(); + return; + } + + freq_ = points_.size() / std::max(points_.back().x() - points_.front().x(), 1.0); render(sig->color, range, size); } void Sparkline::render(const QColor &color, int range, QSize size) { - const double xscale = (size.width() - 1) / (double)range; - const double yscale = (size.height() - 3) / (max_val - min_val); - for (auto &v : points) { - v = QPoint(v.x() * xscale, 1 + std::abs(v.y() - max_val) * yscale); + // Adjust for flat lines + bool is_flat_line = min_val == max_val; + if (is_flat_line) { + min_val -= 1.0; + max_val += 1.0; } + // Calculate scaling + const double xscale = (size.width() - 1) / (double)range; + const double yscale = (size.height() - 3) / (max_val - min_val); + bool draw_individual_points = (points_.back().x() * xscale / points_.size()) > 8.0; + + // Transform or downsample points + render_points_.reserve(points_.size()); + render_points_.clear(); + if (draw_individual_points) { + for (const auto &p : points_) { + render_points_.emplace_back(p.x() * xscale, 1.0 + (max_val - p.y()) * yscale); + } + } else if (is_flat_line) { + double y = size.height() / 2.0; + render_points_.emplace_back(0.0, y); + render_points_.emplace_back(points_.back().x() * xscale, y); + } else { + double prev_y = points_.front().y(); + render_points_.emplace_back(points_.front().x() * xscale, 1.0 + (max_val - prev_y) * yscale); + bool in_flat = false; + + for (size_t i = 1; i < points_.size(); ++i) { + const auto &p = points_[i]; + double y = p.y(); + if (std::abs(y - prev_y) < 1e-6) { + in_flat = true; + } else { + if (in_flat) render_points_.emplace_back(points_[i - 1].x() * xscale, 1.0 + (max_val - prev_y) * yscale); + render_points_.emplace_back(p.x() * xscale, 1.0 + (max_val - y) * yscale); + in_flat = false; + } + prev_y = y; + } + if (in_flat) render_points_.emplace_back(points_.back().x() * xscale, 1.0 + (max_val - prev_y) * yscale); + } + + // Render to pixmap qreal dpr = qApp->devicePixelRatio(); - size *= dpr; - if (size != pixmap.size()) { - pixmap = QPixmap(size); + const QSize pixmap_size = size * dpr; + if (pixmap.size() != pixmap_size) { + pixmap = QPixmap(pixmap_size); } pixmap.setDevicePixelRatio(dpr); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); - painter.setRenderHint(QPainter::Antialiasing, points.size() < 500); + painter.setRenderHint(QPainter::Antialiasing, render_points_.size() <= 500); painter.setPen(color); - painter.drawPolyline(points.data(), points.size()); + painter.drawPolyline(render_points_.data(), render_points_.size()); + painter.setPen(QPen(color, 3)); - if ((points.back().x() - points.front().x()) / points.size() > 8) { - painter.drawPoints(points.data(), points.size()); + if (draw_individual_points) { + painter.drawPoints(render_points_.data(), render_points_.size()); } else { - painter.drawPoint(points.back()); + painter.drawPoint(render_points_.back()); } } diff --git a/tools/cabana/chart/sparkline.h b/tools/cabana/chart/sparkline.h index 806f0a61eb..7f30047d0d 100644 --- a/tools/cabana/chart/sparkline.h +++ b/tools/cabana/chart/sparkline.h @@ -9,7 +9,7 @@ class Sparkline { public: - void update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size); + void update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, QSize size); inline double freq() const { return freq_; } bool isEmpty() const { return pixmap.isNull(); } @@ -20,6 +20,7 @@ public: private: void render(const QColor &color, int range, QSize size); - std::vector points; + std::vector points_; + std::vector render_points_; double freq_ = 0; }; diff --git a/tools/cabana/chart/tiplabel.cc b/tools/cabana/chart/tiplabel.cc index be71602838..233fa80373 100644 --- a/tools/cabana/chart/tiplabel.cc +++ b/tools/cabana/chart/tiplabel.cc @@ -7,6 +7,7 @@ #include #include "tools/cabana/settings.h" +#include "tools/cabana/utils/util.h" TipLabel::TipLabel(QWidget *parent) : QLabel(parent, Qt::ToolTip | Qt::FramelessWindowHint) { setAttribute(Qt::WA_ShowWithoutActivating); @@ -19,7 +20,7 @@ TipLabel::TipLabel(QWidget *parent) : QLabel(parent, Qt::ToolTip | Qt::Frameless font.setPointSizeF(8.34563465); setFont(font); auto palette = QToolTip::palette(); - if (settings.theme != DARK_THEME) { + if (!utils::isDarkTheme()) { palette.setColor(QPalette::ToolTipBase, QApplication::palette().color(QPalette::Base)); palette.setColor(QPalette::ToolTipText, QRgb(0x404044)); // same color as chart label brush } diff --git a/tools/cabana/dbc/dbc.cc b/tools/cabana/dbc/dbc.cc index e9c869fce7..9b0de92218 100644 --- a/tools/cabana/dbc/dbc.cc +++ b/tools/cabana/dbc/dbc.cc @@ -109,7 +109,7 @@ void cabana::Msg::update() { mask[i] |= ((1ULL << sz) - 1) << shift; - bits -= size; + bits -= sz; i = sig->is_little_endian ? i - 1 : i + 1; } } diff --git a/tools/cabana/historylog.cc b/tools/cabana/historylog.cc index 7e73da55f0..3dbdf5a7cd 100644 --- a/tools/cabana/historylog.cc +++ b/tools/cabana/historylog.cc @@ -153,7 +153,7 @@ void HeaderView::paintSection(QPainter *painter, const QRect &rect, int logicalI painter->fillRect(rect, bg_role.value()); } QString text = model()->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString(); - painter->setPen(palette().color(settings.theme == DARK_THEME ? QPalette::BrightText : QPalette::Text)); + painter->setPen(palette().color(utils::isDarkTheme() ? QPalette::BrightText : QPalette::Text)); painter->drawText(rect.adjusted(5, 3, -5, -3), defaultAlignment(), text.replace(QChar('_'), ' ')); } diff --git a/tools/cabana/panda.cc b/tools/cabana/panda.cc new file mode 100644 index 0000000000..0612d67746 --- /dev/null +++ b/tools/cabana/panda.cc @@ -0,0 +1,346 @@ +#include "tools/cabana/panda.h" + +#include +#include +#include +#include +#include + +#include "cereal/messaging/messaging.h" +#include "common/swaglog.h" +#include "common/util.h" + +static libusb_context *init_usb_ctx() { + libusb_context *context = nullptr; + int err = libusb_init(&context); + if (err != 0) { + LOGE("libusb initialization error"); + return nullptr; + } + +#if LIBUSB_API_VERSION >= 0x01000106 + libusb_set_option(context, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_INFO); +#else + libusb_set_debug(context, 3); +#endif + return context; +} + +Panda::Panda(std::string serial, uint32_t bus_offset) : bus_offset(bus_offset) { + if (!init_usb_connection(serial)) { + throw std::runtime_error("Error connecting to panda"); + } + + LOGW("connected to %s over USB", serial.c_str()); + hw_type = get_hw_type(); + can_reset_communications(); +} + +Panda::~Panda() { + cleanup_usb(); +} + +bool Panda::connected() { + return connected_flag; +} + +bool Panda::comms_healthy() { + return comms_healthy_flag; +} + +std::string Panda::hw_serial() { + return hw_serial_str; +} + +std::vector Panda::list(bool usb_only) { + static std::unique_ptr context(init_usb_ctx(), libusb_exit); + + ssize_t num_devices; + libusb_device **dev_list = NULL; + std::vector serials; + if (!context) { return serials; } + + num_devices = libusb_get_device_list(context.get(), &dev_list); + if (num_devices < 0) { + LOGE("libusb can't get device list"); + goto finish; + } + + for (size_t i = 0; i < num_devices; ++i) { + libusb_device *device = dev_list[i]; + libusb_device_descriptor desc; + libusb_get_device_descriptor(device, &desc); + if (desc.idVendor == 0x3801 && desc.idProduct == 0xddcc) { + libusb_device_handle *handle = NULL; + int ret = libusb_open(device, &handle); + if (ret < 0) { goto finish; } + + unsigned char desc_serial[26] = { 0 }; + ret = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, desc_serial, std::size(desc_serial)); + libusb_close(handle); + if (ret < 0) { goto finish; } + + serials.push_back(std::string((char *)desc_serial, ret)); + } + } + +finish: + if (dev_list != NULL) { + libusb_free_device_list(dev_list, 1); + } + return serials; +} + +void Panda::set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param) { + control_write(0xdc, (uint16_t)safety_model, safety_param); +} + + +cereal::PandaState::PandaType Panda::get_hw_type() { + unsigned char hw_query[1] = {0}; + + control_read(0xc1, 0, 0, hw_query, 1); + return (cereal::PandaState::PandaType)(hw_query[0]); +} + + + + +void Panda::send_heartbeat(bool engaged) { + control_write(0xf3, engaged, 0); +} + +void Panda::set_can_speed_kbps(uint16_t bus, uint16_t speed) { + control_write(0xde, bus, (speed * 10)); +} + + +void Panda::set_data_speed_kbps(uint16_t bus, uint16_t speed) { + control_write(0xf9, bus, (speed * 10)); +} + + + +bool Panda::can_receive(std::vector& out_vec) { + // Check if enough space left in buffer to store RECV_SIZE data + assert(receive_buffer_size + RECV_SIZE <= sizeof(receive_buffer)); + + int recv = bulk_read(0x81, &receive_buffer[receive_buffer_size], RECV_SIZE); + if (!comms_healthy()) { + return false; + } + + bool ret = true; + if (recv > 0) { + receive_buffer_size += recv; + ret = unpack_can_buffer(receive_buffer, receive_buffer_size, out_vec); + } + return ret; +} + +void Panda::can_reset_communications() { + control_write(0xc0, 0, 0); +} + +bool Panda::unpack_can_buffer(uint8_t *data, uint32_t &size, std::vector &out_vec) { + int pos = 0; + + while (pos <= size - sizeof(can_header)) { + can_header header; + memcpy(&header, &data[pos], sizeof(can_header)); + + const uint8_t data_len = dlc_to_len[header.data_len_code]; + if (pos + sizeof(can_header) + data_len > size) { + // we don't have all the data for this message yet + break; + } + + if (calculate_checksum(&data[pos], sizeof(can_header) + data_len) != 0) { + LOGE("Panda CAN checksum failed"); + size = 0; + can_reset_communications(); + return false; + } + + can_frame &canData = out_vec.emplace_back(); + canData.address = header.addr; + canData.src = header.bus + bus_offset; + if (header.rejected) { + canData.src += CAN_REJECTED_BUS_OFFSET; + } + if (header.returned) { + canData.src += CAN_RETURNED_BUS_OFFSET; + } + + canData.dat.assign((char *)&data[pos + sizeof(can_header)], data_len); + + pos += sizeof(can_header) + data_len; + } + + // move the overflowing data to the beginning of the buffer for the next round + memmove(data, &data[pos], size - pos); + size -= pos; + + return true; +} + +uint8_t Panda::calculate_checksum(uint8_t *data, uint32_t len) { + uint8_t checksum = 0U; + for (uint32_t i = 0U; i < len; i++) { + checksum ^= data[i]; + } + return checksum; +} + +// USB implementation methods +bool Panda::init_usb_connection(const std::string& serial) { + ssize_t num_devices; + libusb_device **dev_list = NULL; + int err = 0; + + ctx = init_usb_ctx(); + if (!ctx) { goto fail; } + + // connect by serial + num_devices = libusb_get_device_list(ctx, &dev_list); + if (num_devices < 0) { goto fail; } + + for (size_t i = 0; i < num_devices; ++i) { + libusb_device_descriptor desc; + libusb_get_device_descriptor(dev_list[i], &desc); + if (desc.idVendor == 0x3801 && desc.idProduct == 0xddcc) { + int ret = libusb_open(dev_list[i], &dev_handle); + if (dev_handle == NULL || ret < 0) { goto fail; } + + unsigned char desc_serial[26] = { 0 }; + ret = libusb_get_string_descriptor_ascii(dev_handle, desc.iSerialNumber, desc_serial, std::size(desc_serial)); + if (ret < 0) { goto fail; } + + hw_serial_str = std::string((char *)desc_serial, ret); + if (serial.empty() || serial == hw_serial_str) { + break; + } + libusb_close(dev_handle); + dev_handle = NULL; + } + } + if (dev_handle == NULL) goto fail; + libusb_free_device_list(dev_list, 1); + + if (libusb_kernel_driver_active(dev_handle, 0) == 1) { + libusb_detach_kernel_driver(dev_handle, 0); + } + + err = libusb_set_configuration(dev_handle, 1); + if (err != 0) { goto fail; } + + err = libusb_claim_interface(dev_handle, 0); + if (err != 0) { goto fail; } + + return true; + +fail: + if (dev_list != NULL) { + libusb_free_device_list(dev_list, 1); + } + cleanup_usb(); + return false; +} + +void Panda::cleanup_usb() { + if (dev_handle) { + libusb_release_interface(dev_handle, 0); + libusb_close(dev_handle); + dev_handle = nullptr; + } + + if (ctx) { + libusb_exit(ctx); + ctx = nullptr; + } + connected_flag = false; +} + +void Panda::handle_usb_issue(int err, const char func[]) { + LOGE_100("usb error %d \"%s\" in %s", err, libusb_strerror((enum libusb_error)err), func); + if (err == LIBUSB_ERROR_NO_DEVICE) { + LOGE("lost connection"); + connected_flag = false; + } +} + +int Panda::control_write(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned int timeout) { + int err; + const uint8_t bmRequestType = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE; + + if (!connected_flag) { + return LIBUSB_ERROR_NO_DEVICE; + } + + do { + err = libusb_control_transfer(dev_handle, bmRequestType, bRequest, wValue, wIndex, NULL, 0, timeout); + if (err < 0) handle_usb_issue(err, __func__); + } while (err < 0 && connected_flag); + + return err; +} + +int Panda::control_read(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned char *data, uint16_t wLength, unsigned int timeout) { + int err; + const uint8_t bmRequestType = LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE; + + if (!connected_flag) { + return LIBUSB_ERROR_NO_DEVICE; + } + + do { + err = libusb_control_transfer(dev_handle, bmRequestType, bRequest, wValue, wIndex, data, wLength, timeout); + if (err < 0) handle_usb_issue(err, __func__); + } while (err < 0 && connected_flag); + + return err; +} + +int Panda::bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { + int err; + int transferred = 0; + + if (!connected_flag) { + return 0; + } + + do { + err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout); + if (err == LIBUSB_ERROR_TIMEOUT) { + LOGW("Transmit buffer full"); + break; + } else if (err != 0 || length != transferred) { + handle_usb_issue(err, __func__); + } + } while (err != 0 && connected_flag); + + return transferred; +} + +int Panda::bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { + int err; + int transferred = 0; + + if (!connected_flag) { + return 0; + } + + do { + err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout); + if (err == LIBUSB_ERROR_TIMEOUT) { + break; // timeout is okay to exit, recv still happened + } else if (err == LIBUSB_ERROR_OVERFLOW) { + comms_healthy_flag = false; + LOGE_100("overflow got 0x%x", transferred); + } else if (err != 0) { + handle_usb_issue(err, __func__); + } + } while (err != 0 && connected_flag); + + return transferred; +} diff --git a/tools/cabana/panda.h b/tools/cabana/panda.h new file mode 100644 index 0000000000..d318c33f4d --- /dev/null +++ b/tools/cabana/panda.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "cereal/gen/cpp/car.capnp.h" +#include "cereal/gen/cpp/log.capnp.h" +#include "panda/board/health.h" +#include "panda/board/can.h" + +#define USB_TX_SOFT_LIMIT (0x100U) +#define USBPACKET_MAX_SIZE (0x40) +#define RECV_SIZE (0x4000U) +#define TIMEOUT 0 + +#define CAN_REJECTED_BUS_OFFSET 0xC0U +#define CAN_RETURNED_BUS_OFFSET 0x80U + +#define PANDA_BUS_OFFSET 4 + +struct __attribute__((packed)) can_header { + uint8_t reserved : 1; + uint8_t bus : 3; + uint8_t data_len_code : 4; + uint8_t rejected : 1; + uint8_t returned : 1; + uint8_t extended : 1; + uint32_t addr : 29; + uint8_t checksum : 8; +}; + +struct can_frame { + long address; + std::string dat; + long src; +}; + + +class Panda { +public: + Panda(std::string serial="", uint32_t bus_offset=0); + ~Panda(); + + cereal::PandaState::PandaType hw_type = cereal::PandaState::PandaType::UNKNOWN; + const uint32_t bus_offset; + + bool connected(); + bool comms_healthy(); + std::string hw_serial(); + + // Static functions + static std::vector list(bool usb_only=false); + + // Panda functionality + cereal::PandaState::PandaType get_hw_type(); + void set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param=0U); + 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); + bool can_receive(std::vector& out_vec); + void can_reset_communications(); + +private: + // USB connection members + libusb_context *ctx = nullptr; + libusb_device_handle *dev_handle = nullptr; + std::string hw_serial_str; + std::atomic connected_flag = true; + std::atomic comms_healthy_flag = true; + + // CAN buffer members + uint8_t receive_buffer[RECV_SIZE + sizeof(can_header) + 64]; + uint32_t receive_buffer_size = 0; + + // Internal methods + bool init_usb_connection(const std::string& serial); + void cleanup_usb(); + void handle_usb_issue(int err, const char func[]); + int control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout=TIMEOUT); + int control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout=TIMEOUT); + int bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); + int bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); + bool unpack_can_buffer(uint8_t *data, uint32_t &size, std::vector &out_vec); + uint8_t calculate_checksum(uint8_t *data, uint32_t len); +}; diff --git a/tools/cabana/signalview.cc b/tools/cabana/signalview.cc index 9fe70c3ee8..35537d75e3 100644 --- a/tools/cabana/signalview.cc +++ b/tools/cabana/signalview.cc @@ -634,11 +634,12 @@ void SignalView::updateState(const std::set *msgs) { QSize size(available_width - value_width, delegate->button_size.height() - style()->pixelMetric(QStyle::PM_FocusFrameVMargin) * 2); + auto [first, last] = can->eventsInRange(model->msg_id, std::make_pair(last_msg.ts -settings.sparkline_range, last_msg.ts)); QFutureSynchronizer synchronizer; for (int i = first_visible.row(); i <= last_visible.row(); ++i) { auto item = model->getItem(model->index(i, 1)); synchronizer.addFuture(QtConcurrent::run( - &item->sparkline, &Sparkline::update, model->msg_id, item->sig, last_msg.ts, settings.sparkline_range, size)); + &item->sparkline, &Sparkline::update, item->sig, first, last, settings.sparkline_range, size)); } synchronizer.waitForFinished(); } diff --git a/tools/cabana/streams/pandastream.cc b/tools/cabana/streams/pandastream.cc index e66d72e59d..a2430c665f 100644 --- a/tools/cabana/streams/pandastream.cc +++ b/tools/cabana/streams/pandastream.cc @@ -24,7 +24,7 @@ bool PandaStream::connect() { return false; } - panda->set_safety_model(cereal::CarParams::SafetyModel::SILENT); + panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT); for (int bus = 0; bus < config.bus_config.size(); bus++) { panda->set_can_speed_kbps(bus, config.bus_config[bus].can_speed_kbps); @@ -72,7 +72,7 @@ void PandaStream::streamThread() { handleEvent(capnp::messageToFlatArray(msg)); - panda->send_heartbeat(false, false); + panda->send_heartbeat(false); } } diff --git a/tools/cabana/streams/pandastream.h b/tools/cabana/streams/pandastream.h index 826b1aa986..e17ad887fc 100644 --- a/tools/cabana/streams/pandastream.h +++ b/tools/cabana/streams/pandastream.h @@ -7,7 +7,7 @@ #include #include "tools/cabana/streams/livestream.h" -#include "selfdrive/pandad/panda.h" +#include "tools/cabana/panda.h" const uint32_t speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U}; const uint32_t data_speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U, 2000U, 5000U}; diff --git a/tools/cabana/utils/util.cc b/tools/cabana/utils/util.cc index 4556f90850..f27df4bf1e 100644 --- a/tools/cabana/utils/util.cc +++ b/tools/cabana/utils/util.cc @@ -191,8 +191,15 @@ DoubleValidator::DoubleValidator(QObject *parent) : QDoubleValidator(parent) { } namespace utils { + +bool isDarkTheme() { + QColor windowColor = QApplication::palette().color(QPalette::Window); + return windowColor.lightness() < 128; +} + QPixmap icon(const QString &id) { - bool dark_theme = settings.theme == DARK_THEME; + bool dark_theme = isDarkTheme(); + QPixmap pm; QString key = "bootstrap_" % id % (dark_theme ? "1" : "0"); if (!QPixmapCache::find(key, &pm)) { diff --git a/tools/cabana/utils/util.h b/tools/cabana/utils/util.h index 1aad3103db..5c1cbe2d69 100644 --- a/tools/cabana/utils/util.h +++ b/tools/cabana/utils/util.h @@ -98,7 +98,9 @@ public: }; namespace utils { + QPixmap icon(const QString &id); +bool isDarkTheme(); void setTheme(int theme); QString formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false); inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) { diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index 3d715b0319..c857c9ffbe 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -19,7 +19,7 @@ const int THUMBNAIL_MARGIN = 3; static const QColor timeline_colors[] = { [(int)TimelineType::None] = QColor(111, 143, 175), [(int)TimelineType::Engaged] = QColor(0, 163, 108), - [(int)TimelineType::UserFlag] = Qt::magenta, + [(int)TimelineType::UserBookmark] = Qt::magenta, [(int)TimelineType::AlertInfo] = Qt::green, [(int)TimelineType::AlertWarning] = QColor(255, 195, 0), [(int)TimelineType::AlertCritical] = QColor(199, 0, 57), @@ -64,7 +64,7 @@ VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { Pause/Resume:  space  )").arg(timeline_colors[(int)TimelineType::None].name(), timeline_colors[(int)TimelineType::Engaged].name(), - timeline_colors[(int)TimelineType::UserFlag].name(), + timeline_colors[(int)TimelineType::UserBookmark].name(), timeline_colors[(int)TimelineType::AlertInfo].name(), timeline_colors[(int)TimelineType::AlertWarning].name(), timeline_colors[(int)TimelineType::AlertCritical].name())); @@ -261,14 +261,23 @@ void Slider::paintEvent(QPaintEvent *ev) { QStyleOptionSlider opt; initStyleOption(&opt); - QRect r = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); - p.fillRect(r, timeline_colors[(int)TimelineType::None]); + QRect handle_rect = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); + QRect groove_rect = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); + + // Adjust groove height to match handle height + int handle_height = handle_rect.height(); + groove_rect.setHeight(handle_height * 0.5); + groove_rect.moveCenter(QPoint(groove_rect.center().x(), rect().center().y())); + + p.fillRect(groove_rect, timeline_colors[(int)TimelineType::None]); double min = minimum() / factor; double max = maximum() / factor; auto fillRange = [&](double begin, double end, const QColor &color) { if (begin > max || end < min) return; + + QRect r = groove_rect; r.setLeft(((std::max(min, begin) - min) / (max - min)) * width()); r.setRight(((std::min(max, end) - min) / (max - min)) * width()); p.fillRect(r, color); diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index 6f756448c0..6da0023123 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -11,7 +11,7 @@ #include #include -#include "selfdrive/ui/qt/widgets/cameraview.h" +#include "tools/cabana/cameraview.h" #include "tools/cabana/utils/util.h" #include "tools/replay/logreader.h" #include "tools/cabana/streams/replaystream.h" diff --git a/tools/camerastream/compressed_vipc.py b/tools/camerastream/compressed_vipc.py index 8c773f9dfe..b25b8b0cb7 100755 --- a/tools/camerastream/compressed_vipc.py +++ b/tools/camerastream/compressed_vipc.py @@ -62,7 +62,7 @@ def decoder(addr, vipc_server, vst, nvidia, W, H, debug=False): print("waiting for iframe") continue time_q.append(time.monotonic()) - network_latency = (int(time.time()*1e9) - evta.unixTimestampNanos)/1e6 + network_latency = (int(time.time()*1e9) - evta.unixTimestampNanos)/1e6 # noqa: TID251 frame_latency = ((evta.idx.timestampEof/1e9) - (evta.idx.timestampSof/1e9))*1000 process_latency = ((evt.logMonoTime/1e9) - (evta.idx.timestampEof/1e9))*1000 diff --git a/tools/car_porting/examples/find_segments_with_message.ipynb b/tools/car_porting/examples/find_segments_with_message.ipynb index 4e688cc65b..af17bde52b 100644 --- a/tools/car_porting/examples/find_segments_with_message.ipynb +++ b/tools/car_porting/examples/find_segments_with_message.ipynb @@ -76,7 +76,7 @@ " if platform not in database:\n", " print(f\"No segments available for {platform}\")\n", " continue\n", - " \n", + "\n", " all_segments = database[platform]\n", " NUM_SEGMENTS = min(len(all_segments), MAX_SEGS_PER_PLATFORM)\n", " TEST_SEGMENTS.extend(random.sample(all_segments, NUM_SEGMENTS))\n", @@ -147,7 +147,7 @@ } ], "source": [ - "from openpilot.tools.lib.logreader import LogReader\n", + "from openpilot.tools.lib.logreader import LogReader, comma_car_segments_source\n", "from tqdm.notebook import tqdm, tnrange\n", "\n", "# Example search for CAN ignition messages\n", @@ -169,7 +169,7 @@ "progress_bar = tnrange(len(TEST_SEGMENTS), desc=\"segments searched\")\n", "\n", "for segment in TEST_SEGMENTS:\n", - " lr = LogReader(segment)\n", + " lr = LogReader(segment, sources=[comma_car_segments_source])\n", " CP = lr.first(\"carParams\")\n", " if CP is None:\n", " progress_bar.update()\n", diff --git a/tools/car_porting/examples/ford_vin_fingerprint.ipynb b/tools/car_porting/examples/ford_vin_fingerprint.ipynb index 7b0dd656da..6b806d22d2 100644 --- a/tools/car_porting/examples/ford_vin_fingerprint.ipynb +++ b/tools/car_porting/examples/ford_vin_fingerprint.ipynb @@ -20,7 +20,7 @@ "source": [ "\"\"\"In this example, we use the public comma car segments database to check if vin fingerprinting is feasible for ford.\"\"\"\n", "\n", - "from openpilot.tools.lib.logreader import LogReader\n", + "from openpilot.tools.lib.logreader import LogReader, comma_car_segments_source\n", "from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database\n", "from opendbc.car.ford.values import CAR\n", "\n", @@ -100,7 +100,7 @@ " if platform not in database:\n", " print(f\"Skipping platform: {platform}, no data available\")\n", " continue\n", - " \n", + "\n", " all_segments = database[platform]\n", "\n", " NUM_SEGMENTS = min(len(all_segments), MAX_SEGS_PER_PLATFORM)\n", @@ -110,7 +110,7 @@ " segments = random.sample(all_segments, NUM_SEGMENTS)\n", "\n", " for segment in segments:\n", - " lr = LogReader(segment)\n", + " lr = LogReader(segment, sources=[comma_car_segments_source])\n", " CP = lr.first(\"carParams\")\n", " if \"FORD\" not in CP.carFingerprint:\n", " print(segment, CP.carFingerprint)\n", diff --git a/tools/car_porting/examples/hkg_canfd_gear_message.ipynb b/tools/car_porting/examples/hkg_canfd_gear_message.ipynb index 5fdbdda684..f0bca8decc 100644 --- a/tools/car_porting/examples/hkg_canfd_gear_message.ipynb +++ b/tools/car_porting/examples/hkg_canfd_gear_message.ipynb @@ -72,7 +72,7 @@ " #if platform not in database:\n", " # print(f\"Skipping platform: {platform}, no data available\")\n", " # continue\n", - " \n", + "\n", " all_segments = database[platform]\n", "\n", " NUM_SEGMENTS = min(len(all_segments), MAX_SEGS_PER_PLATFORM)\n", @@ -198,12 +198,12 @@ "from opendbc.car.hyundai.hyundaicanfd import CanBus\n", "\n", "from openpilot.selfdrive.pandad import can_capnp_to_list\n", - "from openpilot.tools.lib.logreader import LogReader\n", + "from openpilot.tools.lib.logreader import LogReader, comma_car_segments_source\n", "\n", "message_names = [\"GEAR_SHIFTER\", \"ACCELERATOR\", \"GEAR\", \"GEAR_ALT\", \"GEAR_ALT_2\"]\n", "\n", "for segment in TEST_SEGMENTS:\n", - " lr = LogReader(segment)\n", + " lr = LogReader(segment, sources=[comma_car_segments_source])\n", " CP = lr.first(\"carParams\")\n", " if CP is None:\n", " continue\n", diff --git a/tools/clip/run.py b/tools/clip/run.py index a84290f9c4..7920751447 100755 --- a/tools/clip/run.py +++ b/tools/clip/run.py @@ -29,7 +29,7 @@ FRAMERATE = 20 PIXEL_DEPTH = '24' RESOLUTION = '2160x1080' SECONDS_TO_WARM = 2 -PROC_WAIT_SECONDS = 30 +PROC_WAIT_SECONDS = 30*10 OPENPILOT_FONT = str(Path(BASEDIR, 'selfdrive/assets/fonts/Inter-Regular.ttf').resolve()) REPLAY = str(Path(BASEDIR, 'tools/replay/replay').resolve()) @@ -130,7 +130,7 @@ def populate_car_params(lr: LogReader): for cp in entries: key, value = cp.key, cp.value try: - params.put(key, value) + params.put(key, params.cpp2python(key, value)) except UnknownKeyName: # forks of openpilot may have other Params keys configured. ignore these logger.warning(f"unknown Params key '{key}', skipping") @@ -188,6 +188,7 @@ def clip( out: str, start: int, end: int, + speed: int, target_mb: int, title: str | None, ): @@ -212,6 +213,12 @@ def clip( if title: overlays.append(f"drawtext=text='{escape_ffmpeg_text(title)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=32:{box_style}:x=(w-text_w)/2:y=53") + if speed > 1: + overlays += [ + f"setpts=PTS/{speed}", + "fps=60", + ] + ffmpeg_cmd = [ 'ffmpeg', '-y', '-video_size', RESOLUTION, @@ -289,6 +296,7 @@ def main(): p.add_argument('-o', '--output', help='output clip to (.mp4)', type=validate_output_file, default=DEFAULT_OUTPUT) p.add_argument('-p', '--prefix', help='openpilot prefix', default=f'clip_{randint(100, 99999)}') p.add_argument('-q', '--quality', help='quality of camera (low = qcam, high = hevc)', choices=['low', 'high'], default='high') + p.add_argument('-x', '--speed', help='record the clip at this speed multiple', type=int, default=1) p.add_argument('-s', '--start', help='start clipping at seconds', type=int) p.add_argument('-t', '--title', help='overlay this title on the video (e.g. "Chill driving across the Golden Gate Bridge")', type=validate_title) args = parse_args(p) @@ -302,6 +310,7 @@ def main(): out=args.output, start=args.start, end=args.end, + speed=args.speed, target_mb=args.file_size, title=args.title, ) diff --git a/tools/lib/api.py b/tools/lib/api.py index c2e1b1a8cd..c6e2d98914 100644 --- a/tools/lib/api.py +++ b/tools/lib/api.py @@ -2,6 +2,8 @@ import os import requests API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com') +# TODO: this should be merged into common.api + class CommaApi: def __init__(self, token=None): self.session = requests.Session() diff --git a/tools/lib/comma_car_segments.py b/tools/lib/comma_car_segments.py index fe9c350a9b..cd19356d66 100644 --- a/tools/lib/comma_car_segments.py +++ b/tools/lib/comma_car_segments.py @@ -14,7 +14,8 @@ def get_comma_car_segments_database(): ret = {} for platform in database: - ret[MIGRATION.get(platform, platform)] = database[platform] + # TODO: remove this when commaCarSegments is updated to remove selector + ret[MIGRATION.get(platform, platform)] = [s.rstrip('/s') for s in database[platform]] return ret @@ -86,5 +87,5 @@ def get_repo_url(path): return get_repo_raw_url(path) -def get_url(route, segment, file="rlog.bz2"): +def get_url(route, segment, file="rlog.zst"): return get_repo_url(f"segments/{route.replace('|', '/')}/{segment}/{file}") diff --git a/tools/lib/filereader.py b/tools/lib/filereader.py index 0bb4abd2fa..02f5fd1b95 100644 --- a/tools/lib/filereader.py +++ b/tools/lib/filereader.py @@ -1,6 +1,8 @@ import os import posixpath import socket +from functools import cache +from openpilot.common.retry import retry from urllib.parse import urlparse from openpilot.tools.lib.url_file import URLFile @@ -8,14 +10,16 @@ from openpilot.tools.lib.url_file import URLFile DATA_ENDPOINT = os.getenv("DATA_ENDPOINT", "http://data-raw.comma.internal/") -def internal_source_available(url=DATA_ENDPOINT): +@cache +@retry(delay=0.0) +def internal_source_available(url: str) -> bool: if os.path.isdir(url): return True try: hostname = urlparse(url).hostname port = urlparse(url).port or 80 - with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.settimeout(0.5) s.connect((hostname, port)) return True @@ -30,6 +34,7 @@ def resolve_name(fn): return fn +@cache def file_exists(fn): fn = resolve_name(fn) if fn.startswith(("http://", "https://")): diff --git a/tools/lib/framereader.py b/tools/lib/framereader.py index 8f7f723c28..30ce1f80fe 100644 --- a/tools/lib/framereader.py +++ b/tools/lib/framereader.py @@ -56,6 +56,7 @@ def decompress_video_data(rawdat, w, h, pix_fmt="rgb24", vid_fmt='hevc') -> np.n "-"] dat = subprocess.check_output(args, input=rawdat) + ret: np.ndarray if pix_fmt == "rgb24": ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, h, w, 3) elif pix_fmt in ["nv12", "yuv420p"]: diff --git a/tools/lib/helpers.py b/tools/lib/helpers.py index 7c34e17cb1..8c976e7ecc 100644 --- a/tools/lib/helpers.py +++ b/tools/lib/helpers.py @@ -9,7 +9,7 @@ class RE: INDEX = r'-?[0-9]+' SLICE = fr'(?P{INDEX})?:?(?P{INDEX})?:?(?P{INDEX})?' - SEGMENT_RANGE = fr'{ROUTE_NAME}(?:(--|/)(?P({SLICE})))?(?:/(?P([qras])))?' + SEGMENT_RANGE = fr'{ROUTE_NAME}(?:(--|/)(?P({SLICE})))?(?:/(?P([qra])))?' BOOTLOG_NAME = ROUTE_NAME diff --git a/tools/lib/logreader.py b/tools/lib/logreader.py index 34d3e5ea9f..cc84c8b52e 100755 --- a/tools/lib/logreader.py +++ b/tools/lib/logreader.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import bz2 -from functools import cache, partial +from functools import partial import multiprocessing import capnp import enum @@ -13,14 +13,15 @@ import warnings import zstandard as zstd from collections.abc import Callable, Iterable, Iterator +from typing import cast from urllib.parse import parse_qs, urlparse from cereal import log as capnp_log from openpilot.common.swaglog import cloudlog from openpilot.tools.lib.comma_car_segments import get_url as get_comma_segments_url from openpilot.tools.lib.openpilotci import get_url -from openpilot.tools.lib.filereader import FileReader, file_exists, internal_source_available -from openpilot.tools.lib.route import Route, SegmentRange +from openpilot.tools.lib.filereader import DATA_ENDPOINT, FileReader, file_exists, internal_source_available +from openpilot.tools.lib.route import Route, SegmentRange, FileName from openpilot.tools.lib.log_time_series import msgs_to_time_series LogMessage = type[capnp._DynamicStructReader] @@ -48,8 +49,46 @@ def decompress_stream(data: bytes): return decompressed_data + +class CachedEventReader: + __slots__ = ('_evt', '_enum') + + def __init__(self, evt: capnp._DynamicStructReader, _enum: str | None = None): + """All capnp attribute accesses are expensive, and which() is often called multiple times""" + self._evt = evt + self._enum: str | None = _enum + + # fast pickle support + def __reduce__(self): + return CachedEventReader._reducer, (self._evt.as_builder().to_bytes(), self._enum) + + @staticmethod + def _reducer(data: bytes, _enum: str | None = None): + with capnp_log.Event.from_bytes(data) as evt: + return CachedEventReader(evt, _enum) + + def __repr__(self): + return self._evt.__repr__() + + def __str__(self): + return self._evt.__str__() + + def __dir__(self): + return dir(self._evt) + + def which(self) -> str: + if self._enum is None: + self._enum = self._evt.which() + return self._enum + + def __getattr__(self, name: str): + if name.startswith("__") and name.endswith("__"): + return getattr(self, name) + return getattr(self._evt, name) + + class _LogFileReader: - def __init__(self, fn, canonicalize=True, only_union_types=False, sort_by_time=False, dat=None): + def __init__(self, fn, only_union_types=False, sort_by_time=False, dat=None): self.data_version = None self._only_union_types = only_union_types @@ -74,7 +113,7 @@ class _LogFileReader: self._ents = [] try: for e in ents: - self._ents.append(e) + self._ents.append(CachedEventReader(e)) except capnp.KjException: warnings.warn("Corrupted events detected", RuntimeWarning, stacklevel=1) @@ -96,14 +135,13 @@ class _LogFileReader: class ReadMode(enum.StrEnum): RLOG = "r" # only read rlogs QLOG = "q" # only read qlogs - SANITIZED = "s" # read from the commaCarSegments database AUTO = "a" # default to rlogs, fallback to qlogs AUTO_INTERACTIVE = "i" # default to rlogs, fallback to qlogs with a prompt from the user LogPath = str | None -ValidFileCallable = Callable[[LogPath], bool] -Source = Callable[[SegmentRange, ReadMode], list[LogPath]] +LogFileName = tuple[str, ...] +Source = Callable[[SegmentRange, LogFileName], list[LogPath]] InternalUnavailableException = Exception("Internal source not available") @@ -112,139 +150,129 @@ class LogsUnavailable(Exception): pass -@cache -def default_valid_file(fn: LogPath) -> bool: - return fn is not None and file_exists(fn) - - -def auto_strategy(rlog_paths: list[LogPath], qlog_paths: list[LogPath], interactive: bool, valid_file: ValidFileCallable) -> list[LogPath]: - # auto select logs based on availability - missing_rlogs = [rlog is None or not valid_file(rlog) for rlog in rlog_paths].count(True) - if missing_rlogs != 0: - if interactive: - if input(f"{missing_rlogs}/{len(rlog_paths)} rlogs were not found, would you like to fallback to qlogs for those segments? (y/n) ").lower() != "y": - return rlog_paths - else: - cloudlog.warning(f"{missing_rlogs}/{len(rlog_paths)} rlogs were not found, falling back to qlogs for those segments...") - - return [rlog if valid_file(rlog) else (qlog if valid_file(qlog) else None) - for (rlog, qlog) in zip(rlog_paths, qlog_paths, strict=True)] - return rlog_paths - - -def apply_strategy(mode: ReadMode, rlog_paths: list[LogPath], qlog_paths: list[LogPath], valid_file: ValidFileCallable = default_valid_file) -> list[LogPath]: - if mode == ReadMode.RLOG: - return rlog_paths - elif mode == ReadMode.QLOG: - return qlog_paths - elif mode == ReadMode.AUTO: - return auto_strategy(rlog_paths, qlog_paths, False, valid_file) - elif mode == ReadMode.AUTO_INTERACTIVE: - return auto_strategy(rlog_paths, qlog_paths, True, valid_file) - raise ValueError(f"invalid mode: {mode}") - - -def comma_api_source(sr: SegmentRange, mode: ReadMode) -> list[LogPath]: +def comma_api_source(sr: SegmentRange, fns: LogFileName) -> list[LogPath]: route = Route(sr.route_name) - rlog_paths = [route.log_paths()[seg] for seg in sr.seg_idxs] - qlog_paths = [route.qlog_paths()[seg] for seg in sr.seg_idxs] - # comma api will have already checked if the file exists - def valid_file(fn): - return fn is not None - - return apply_strategy(mode, rlog_paths, qlog_paths, valid_file=valid_file) + if fns == FileName.RLOG: + return [route.log_paths()[seg] for seg in sr.seg_idxs] + else: + return [route.qlog_paths()[seg] for seg in sr.seg_idxs] -def internal_source(sr: SegmentRange, mode: ReadMode, file_ext: str = "bz2") -> list[LogPath]: - if not internal_source_available(): +def internal_source(sr: SegmentRange, fns: LogFileName, endpoint_url: str = DATA_ENDPOINT) -> list[LogPath]: + if not internal_source_available(endpoint_url): raise InternalUnavailableException def get_internal_url(sr: SegmentRange, seg, file): - return f"cd:/{sr.dongle_id}/{sr.log_id}/{seg}/{file}.{file_ext}" + return f"{endpoint_url.rstrip('/')}/{sr.dongle_id}/{sr.log_id}/{seg}/{file}" - # TODO: list instead of using static URLs to support routes with multiple file extensions - rlog_paths = [get_internal_url(sr, seg, "rlog") for seg in sr.seg_idxs] - qlog_paths = [get_internal_url(sr, seg, "qlog") for seg in sr.seg_idxs] - - return apply_strategy(mode, rlog_paths, qlog_paths) + return eval_source([[get_internal_url(sr, seg, fn) for fn in fns] for seg in sr.seg_idxs]) -def internal_source_zst(sr: SegmentRange, mode: ReadMode, file_ext: str = "zst") -> list[LogPath]: - return internal_source(sr, mode, file_ext) +def openpilotci_source(sr: SegmentRange, fns: LogFileName) -> list[LogPath]: + return eval_source([[get_url(sr.route_name, seg, fn) for fn in fns] for seg in sr.seg_idxs]) -def openpilotci_source(sr: SegmentRange, mode: ReadMode, file_ext: str = "bz2") -> list[LogPath]: - rlog_paths = [get_url(sr.route_name, seg, f"rlog.{file_ext}") for seg in sr.seg_idxs] - qlog_paths = [get_url(sr.route_name, seg, f"qlog.{file_ext}") for seg in sr.seg_idxs] - - return apply_strategy(mode, rlog_paths, qlog_paths) +def comma_car_segments_source(sr: SegmentRange, fns: LogFileName) -> list[LogPath]: + return eval_source([get_comma_segments_url(sr.route_name, seg) for seg in sr.seg_idxs]) -def openpilotci_source_zst(sr: SegmentRange, mode: ReadMode) -> list[LogPath]: - return openpilotci_source(sr, mode, "zst") - - -def comma_car_segments_source(sr: SegmentRange, mode=ReadMode.RLOG) -> list[LogPath]: - return [get_comma_segments_url(sr.route_name, seg) for seg in sr.seg_idxs] - - -def testing_closet_source(sr: SegmentRange, mode=ReadMode.RLOG) -> list[LogPath]: - if not internal_source_available('http://testing.comma.life'): - raise InternalUnavailableException - return [f"http://testing.comma.life/download/{sr.route_name.replace('|', '/')}/{seg}/rlog" for seg in sr.seg_idxs] - - -def direct_source(file_or_url: str) -> list[LogPath]: +def direct_source(file_or_url: str) -> list[str]: return [file_or_url] -def get_invalid_files(files): - for f in files: - if f is None or not file_exists(f): - yield f +def eval_source(files: list[list[str] | str]) -> list[LogPath]: + # Returns valid file URLs given a list of possible file URLs for each segment (e.g. rlog.bz2, rlog.zst) + valid_files: list[LogPath] = [] + + for urls in files: + if isinstance(urls, str): + urls = [urls] + + for url in urls: + if file_exists(url): + valid_files.append(url) + break + else: + valid_files.append(None) + + return valid_files -def check_source(source: Source, *args) -> list[LogPath]: - files = source(*args) - assert len(files) > 0, "No files on source" - assert next(get_invalid_files(files), False) is False, "Some files are invalid" - return files - - -def auto_source(sr: SegmentRange, mode=ReadMode.RLOG, sources: list[Source] = None) -> list[LogPath]: - if mode == ReadMode.SANITIZED: - return comma_car_segments_source(sr, mode) - - if sources is None: - sources = [internal_source, internal_source_zst, openpilotci_source, openpilotci_source_zst, - comma_api_source, comma_car_segments_source, testing_closet_source] +def auto_source(identifier: str, sources: list[Source], default_mode: ReadMode) -> list[str]: exceptions = {} - # for automatic fallback modes, auto_source needs to first check if rlogs exist for any source - if mode in [ReadMode.AUTO, ReadMode.AUTO_INTERACTIVE]: + sr = SegmentRange(identifier) + mode = default_mode if sr.selector is None else ReadMode(sr.selector) + + if mode == ReadMode.QLOG: + try_fns = [FileName.QLOG] + else: + try_fns = [FileName.RLOG] + + # If selector allows it, fallback to qlogs + if mode in (ReadMode.AUTO, ReadMode.AUTO_INTERACTIVE): + try_fns.append(FileName.QLOG) + + # Build a dict of valid files as we evaluate each source. May contain mix of rlogs, qlogs, and None. + # This function only returns when we've sourced all files, or throws an exception + valid_files: dict[int, LogPath] = {} + for fn in try_fns: for source in sources: try: - return check_source(source, sr, ReadMode.RLOG) - except Exception: - pass + files = source(sr, fn) - # Automatically determine viable source - for source in sources: - try: - return check_source(source, sr, mode) - except Exception as e: - exceptions[source.__name__] = e + # Check every source returns an expected number of files + assert len(files) == len(valid_files) or len(valid_files) == 0, f"Source {source.__name__} returned unexpected number of files" - raise LogsUnavailable("auto_source could not find any valid source, exceptions for sources:\n - " + - "\n - ".join([f"{k}: {repr(v)}" for k, v in exceptions.items()])) + # Build a dict of valid files + for idx, f in enumerate(files): + if valid_files.get(idx) is None: + valid_files[idx] = f + + # We've found all files, return them + if all(f is not None for f in valid_files.values()): + return cast(list[str], list(valid_files.values())) + + except Exception as e: + exceptions[source.__name__] = e + + if fn == try_fns[0]: + missing_logs = list(valid_files.values()).count(None) + if mode == ReadMode.AUTO: + cloudlog.warning(f"{missing_logs}/{len(valid_files)} rlogs were not found, falling back to qlogs for those segments...") + elif mode == ReadMode.AUTO_INTERACTIVE: + if input(f"{missing_logs}/{len(valid_files)} rlogs were not found, would you like to fallback to qlogs for those segments? (y/N) ").lower() != "y": + break + + missing_logs = list(valid_files.values()).count(None) + raise LogsUnavailable(f"{missing_logs}/{len(valid_files)} logs were not found, please ensure all logs " + + "are uploaded. You can fall back to qlogs with '/a' selector at the end of the route name.\n\n" + + "Exceptions for sources:\n - " + "\n - ".join([f"{k}: {repr(v)}" for k, v in exceptions.items()])) def parse_indirect(identifier: str) -> str: if "useradmin.comma.ai" in identifier: query = parse_qs(urlparse(identifier).query) - return query["onebox"][0] + identifier = query["onebox"][0] + elif "connect.comma.ai" in identifier: + path = urlparse(identifier).path.strip("/").split("/") + path = ['/'.join(path[:2]), *path[2:]] # recombine log id + + identifier = path[0] + if len(path) > 2: + # convert url with seconds to segments + start, end = int(path[1]) // 60, int(path[2]) // 60 + 1 + identifier = f"{identifier}/{start}:{end}" + + # add selector if it exists + if len(path) > 3: + identifier += f"/{path[3]}" + else: + # add selector if it exists + identifier = "/".join(path) + return identifier @@ -255,7 +283,7 @@ def parse_direct(identifier: str): class LogReader: - def _parse_identifier(self, identifier: str) -> list[LogPath]: + def _parse_identifier(self, identifier: str) -> list[str]: # useradmin, etc. identifier = parse_indirect(identifier) @@ -264,20 +292,16 @@ class LogReader: if direct_parsed is not None: return direct_source(identifier) - sr = SegmentRange(identifier) - mode = self.default_mode if sr.selector is None else ReadMode(sr.selector) - - identifiers = self.source(sr, mode) - - invalid_count = len(list(get_invalid_files(identifiers))) - assert invalid_count == 0, (f"{invalid_count}/{len(identifiers)} invalid log(s) found, please ensure all logs " + - "are uploaded or auto fallback to qlogs with '/a' selector at the end of the route name.") + identifiers = auto_source(identifier, self.sources, self.default_mode) return identifiers def __init__(self, identifier: str | list[str], default_mode: ReadMode = ReadMode.RLOG, - source: Source = auto_source, sort_by_time=False, only_union_types=False): + sources: list[Source] = None, sort_by_time=False, only_union_types=False): + if sources is None: + sources = [internal_source, openpilotci_source, comma_api_source, comma_car_segments_source] + self.default_mode = default_mode - self.source = source + self.sources = sources self.identifier = identifier if isinstance(identifier, str): self.identifier = [identifier] diff --git a/tools/lib/route.py b/tools/lib/route.py index 0a4700f083..882585a151 100644 --- a/tools/lib/route.py +++ b/tools/lib/route.py @@ -10,12 +10,15 @@ from openpilot.tools.lib.auth_config import get_token from openpilot.tools.lib.api import APIError, CommaApi from openpilot.tools.lib.helpers import RE -QLOG_FILENAMES = ['qlog', 'qlog.bz2', 'qlog.zst'] -QCAMERA_FILENAMES = ['qcamera.ts'] -LOG_FILENAMES = ['rlog', 'rlog.bz2', 'raw_log.bz2', 'rlog.zst'] -CAMERA_FILENAMES = ['fcamera.hevc', 'video.hevc'] -DCAMERA_FILENAMES = ['dcamera.hevc'] -ECAMERA_FILENAMES = ['ecamera.hevc'] + +class FileName: + RLOG = ("rlog.zst", "rlog.bz2") + QLOG = ("qlog.zst", "qlog.bz2") + QCAMERA = ('qcamera.ts',) + FCAMERA = ('fcamera.hevc',) + ECAMERA = ('ecamera.hevc',) + DCAMERA = ('dcamera.hevc',) + BOOTLOG = ('bootlog.zst', 'bootlog.bz2') class Route: @@ -81,23 +84,23 @@ class Route: if segments.get(segment_name): segments[segment_name] = Segment( segment_name, - url if fn in LOG_FILENAMES else segments[segment_name].log_path, - url if fn in QLOG_FILENAMES else segments[segment_name].qlog_path, - url if fn in CAMERA_FILENAMES else segments[segment_name].camera_path, - url if fn in DCAMERA_FILENAMES else segments[segment_name].dcamera_path, - url if fn in ECAMERA_FILENAMES else segments[segment_name].ecamera_path, - url if fn in QCAMERA_FILENAMES else segments[segment_name].qcamera_path, + url if fn in FileName.RLOG else segments[segment_name].log_path, + url if fn in FileName.QLOG else segments[segment_name].qlog_path, + url if fn in FileName.FCAMERA else segments[segment_name].camera_path, + url if fn in FileName.DCAMERA else segments[segment_name].dcamera_path, + url if fn in FileName.ECAMERA else segments[segment_name].ecamera_path, + url if fn in FileName.QCAMERA else segments[segment_name].qcamera_path, self.metadata['url'], ) else: segments[segment_name] = Segment( segment_name, - url if fn in LOG_FILENAMES else None, - url if fn in QLOG_FILENAMES else None, - url if fn in CAMERA_FILENAMES else None, - url if fn in DCAMERA_FILENAMES else None, - url if fn in ECAMERA_FILENAMES else None, - url if fn in QCAMERA_FILENAMES else None, + url if fn in FileName.RLOG else None, + url if fn in FileName.QLOG else None, + url if fn in FileName.FCAMERA else None, + url if fn in FileName.DCAMERA else None, + url if fn in FileName.ECAMERA else None, + url if fn in FileName.QCAMERA else None, self.metadata['url'], ) @@ -135,32 +138,32 @@ class Route: for segment, files in segment_files.items(): try: - log_path = next(path for path, filename in files if filename in LOG_FILENAMES) + log_path = next(path for path, filename in files if filename in FileName.RLOG) except StopIteration: log_path = None try: - qlog_path = next(path for path, filename in files if filename in QLOG_FILENAMES) + qlog_path = next(path for path, filename in files if filename in FileName.QLOG) except StopIteration: qlog_path = None try: - camera_path = next(path for path, filename in files if filename in CAMERA_FILENAMES) + camera_path = next(path for path, filename in files if filename in FileName.FCAMERA) except StopIteration: camera_path = None try: - dcamera_path = next(path for path, filename in files if filename in DCAMERA_FILENAMES) + dcamera_path = next(path for path, filename in files if filename in FileName.DCAMERA) except StopIteration: dcamera_path = None try: - ecamera_path = next(path for path, filename in files if filename in ECAMERA_FILENAMES) + ecamera_path = next(path for path, filename in files if filename in FileName.ECAMERA) except StopIteration: ecamera_path = None try: - qcamera_path = next(path for path, filename in files if filename in QCAMERA_FILENAMES) + qcamera_path = next(path for path, filename in files if filename in FileName.QCAMERA) except StopIteration: qcamera_path = None @@ -215,12 +218,20 @@ class RouteName: @property def dongle_id(self) -> str: return self._dongle_id + @property + def log_id(self) -> str: return self._time_str + @property def time_str(self) -> str: return self._time_str + @property + def azure_prefix(self): + return f'{self.dongle_id}/{self.log_id}' + def __str__(self) -> str: return self._canonical_name + class SegmentName: # TODO: add constructor that takes dongle_id, time_str, segment_num and then create instances # of this class instead of manually constructing a segment name (use canonical_name prop instead) @@ -241,12 +252,23 @@ class SegmentName: @property def canonical_name(self) -> str: return self._canonical_name + #TODO should only use one name + @property + def data_name(self) -> str: return f"{self._route_name.canonical_name}/{self._num}" + + @property + def azure_prefix(self): + return f'{self.dongle_id}/{self.log_id}/{self._num}' + @property def dongle_id(self) -> str: return self._route_name.dongle_id @property def time_str(self) -> str: return self._route_name.time_str + @property + def log_id(self) -> str: return self._route_name.time_str + @property def segment_num(self) -> int: return self._num @@ -258,6 +280,29 @@ class SegmentName: def __str__(self) -> str: return self._canonical_name + @staticmethod + def from_file_name(file_name): + # ??????/xxxxxxxxxxxxxxxx|1111-11-11-11--11-11-11/1/rlog.bz2 + dongle_id, route_name, segment_num = file_name.replace('|','/').split('/')[-4:-1] + return SegmentName(dongle_id + "|" + route_name + "--" + segment_num) + + @staticmethod + def from_device_key(dongle_id, key): + # 2018-05-07--18-56-13--5/rlog.bz2 + segment_name = key.split('/')[0] + return SegmentName(dongle_id + "|" + segment_name) + + @staticmethod + def from_file_key(key): + # 38c52c217150700f/2018-05-07--18-56-13/5/rlog.bz2 + az_prefix = '/'.join(key.split('/')[:3]) + return SegmentName.from_azure_prefix(az_prefix) + + @staticmethod + def from_azure_prefix(prefix): + # xxxxxxxx/1111-11-11-11--11-11-11/0 + dongle_id, route_name, segment_num = prefix.split("/") + return SegmentName(dongle_id + "|" + route_name + "--" + segment_num) @cache def get_max_seg_number_cached(sr: 'SegmentRange') -> int: @@ -320,3 +365,4 @@ class SegmentRange: def __repr__(self) -> str: return self.__str__() + diff --git a/tools/lib/tests/test_logreader.py b/tools/lib/tests/test_logreader.py index 230b6a65ea..11bdd33ccf 100644 --- a/tools/lib/tests/test_logreader.py +++ b/tools/lib/tests/test_logreader.py @@ -10,7 +10,7 @@ import requests from parameterized import parameterized from cereal import log as capnp_log -from openpilot.tools.lib.logreader import LogIterable, LogReader, comma_api_source, parse_indirect, ReadMode, InternalUnavailableException +from openpilot.tools.lib.logreader import LogsUnavailable, LogIterable, LogReader, comma_api_source, parse_indirect, ReadMode, InternalUnavailableException from openpilot.tools.lib.route import SegmentRange from openpilot.tools.lib.url_file import URLFileException @@ -193,17 +193,17 @@ class TestLogReader: with subtests.test("interactive_yes"): mocker.patch("sys.stdin", new=io.StringIO("y\n")) - lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, source=comma_api_source) + lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, sources=[comma_api_source]) log_len = len(list(lr)) assert qlog_len == log_len with subtests.test("interactive_no"): mocker.patch("sys.stdin", new=io.StringIO("n\n")) - with pytest.raises(AssertionError): - lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, source=comma_api_source) + with pytest.raises(LogsUnavailable): + lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, sources=[comma_api_source]) with subtests.test("non_interactive"): - lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO, source=comma_api_source) + lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO, sources=[comma_api_source]) log_len = len(list(lr)) assert qlog_len == log_len diff --git a/tools/lib/url_file.py b/tools/lib/url_file.py index b2a11dc77b..204726363d 100644 --- a/tools/lib/url_file.py +++ b/tools/lib/url_file.py @@ -131,13 +131,13 @@ class URLFile: download_range = True if self._debug: - t1 = time.time() + t1 = time.monotonic() response = self._request('GET', self._url, headers=headers) ret = response.data if self._debug: - t2 = time.time() + t2 = time.monotonic() if t2 - t1 > 0.1: print(f"get {self._url} {headers!r} {t2 - t1:.3f} slow") diff --git a/tools/longitudinal_maneuvers/maneuversd.py b/tools/longitudinal_maneuvers/maneuversd.py index 6c6e252a57..c17ae23757 100755 --- a/tools/longitudinal_maneuvers/maneuversd.py +++ b/tools/longitudinal_maneuvers/maneuversd.py @@ -3,7 +3,7 @@ import numpy as np from dataclasses import dataclass from cereal import messaging, car -from opendbc.car.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.realtime import DT_MDL from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index d23052d0f0..2a7f83baae 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -50,6 +50,7 @@ brew "zeromq" cask "gcc-arm-embedded" brew "portaudio" brew "gcc@13" +brew "font-noto-color-emoji" EOS echo "[ ] finished brew install t=$SECONDS" diff --git a/tools/op.sh b/tools/op.sh index 9142d50a18..e83b46b2d2 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -231,7 +231,7 @@ function op_setup() { echo "Getting git submodules..." st="$(date +%s)" - if ! git submodule update --filter=blob:none --jobs 4 --init --recursive; then + if ! git submodule update --jobs 4 --init --recursive; then echo -e " ↳ [${RED}✗${NC}] Getting git submodules failed!" loge "ERROR_GIT_SUBMODULES" return 1 @@ -287,6 +287,11 @@ function op_adb() { op_run_command tools/scripts/adb_ssh.sh } +function op_ssh() { + op_before_cmd + op_run_command tools/scripts/ssh.py "$@" +} + function op_check() { VERBOSE=1 op_before_cmd @@ -412,6 +417,7 @@ function op_default() { echo -e " ${BOLD}cabana${NC} Run Cabana" echo -e " ${BOLD}clip${NC} Run clip (linux only)" echo -e " ${BOLD}adb${NC} Run adb shell" + echo -e " ${BOLD}ssh${NC} comma prime SSH helper" echo "" echo -e "${BOLD}${UNDERLINE}Commands [Testing]:${NC}" echo -e " ${BOLD}sim${NC} Run openpilot in a simulator" @@ -471,6 +477,7 @@ function _op() { restart ) shift 1; op_restart "$@" ;; post-commit ) shift 1; op_install_post_commit "$@" ;; adb ) shift 1; op_adb "$@" ;; + ssh ) shift 1; op_ssh "$@" ;; * ) op_default "$@" ;; esac } diff --git a/tools/plotjuggler/README.md b/tools/plotjuggler/README.md index 794c292e68..62905a252d 100644 --- a/tools/plotjuggler/README.md +++ b/tools/plotjuggler/README.md @@ -13,14 +13,13 @@ Once you've [set up the openpilot environment](../README.md), this command will ``` $ ./juggle.py -h usage: juggle.py [-h] [--demo] [--can] [--stream] [--layout [LAYOUT]] [--install] [--dbc DBC] - [route_or_segment_name] [segment_count] + [route_or_segment_name] A helper to run PlotJuggler on openpilot routes positional arguments: route_or_segment_name The route or segment name to plot (cabana share URL accepted) (default: None) - segment_count The number of segments to plot (default: None) optional arguments: -h, --help show this help message and exit @@ -34,16 +33,20 @@ optional arguments: ``` -Examples using route name: +Example using route name: `./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19"` -Examples using segment range: +Examples using segment: `./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19/1"` `./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19/1/q" # use qlogs` +Example using segment range: + +`./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19/0:1"` + ## Streaming Explore live data from your car! Follow these steps to stream from your comma device to your laptop: diff --git a/tools/replay/README.md b/tools/replay/README.md index 8b4afb0acc..72216e2cc8 100644 --- a/tools/replay/README.md +++ b/tools/replay/README.md @@ -75,7 +75,7 @@ Options: --qcam load qcamera --no-hw-decoder disable HW video decoding --no-vipc do not output video - --all do output all messages including uiDebug, userFlag. + --all do output all messages including uiDebug, userBookmark. this may causes issues when used along with UI Arguments: @@ -91,14 +91,6 @@ tools/replay/replay cd selfdrive/ui && ./ui ``` -## Try Radar Point Visualization with Rerun -To visualize radar points, run rp_visualization.py while tools/replay/replay is active. - -```bash -tools/replay/replay -python3 replay/rp_visualization.py -``` - ## Work with plotjuggler If you want to use replay with plotjuggler, you can stream messages by running: diff --git a/tools/replay/consoleui.cc b/tools/replay/consoleui.cc index 871e41c273..185c7d6c36 100644 --- a/tools/replay/consoleui.cc +++ b/tools/replay/consoleui.cc @@ -257,7 +257,7 @@ void ConsoleUI::updateTimeline() { if (entry.type == TimelineType::Engaged) { mvwchgat(win, 1, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL); mvwchgat(win, 2, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL); - } else if (entry.type == TimelineType::UserFlag) { + } else if (entry.type == TimelineType::UserBookmark) { mvwchgat(win, 3, start_pos, end_pos - start_pos + 1, ACS_S3, Color::Cyan, NULL); } else { auto color_id = Color::Green; @@ -329,7 +329,7 @@ void ConsoleUI::handleKey(char c) { } else if (c == 'd') { replay->seekToFlag(FindFlag::nextDisEngagement); } else if (c == 't') { - replay->seekToFlag(FindFlag::nextUserFlag); + replay->seekToFlag(FindFlag::nextUserBookmark); } else if (c == 'i') { replay->seekToFlag(FindFlag::nextInfo); } else if (c == 'w') { diff --git a/tools/replay/framereader.cc b/tools/replay/framereader.cc index 690645aca9..ed88626be3 100644 --- a/tools/replay/framereader.cc +++ b/tools/replay/framereader.cc @@ -80,7 +80,13 @@ bool FrameReader::loadFromFile(CameraType type, const std::string &file, bool no } input_ctx->probesize = 10 * 1024 * 1024; // 10MB - decoder_ = decoder_manager.acquire(type, input_ctx->streams[0]->codecpar, !no_hw_decoder); + video_stream_idx_ = av_find_best_stream(input_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); + if (video_stream_idx_ < 0) { + rError("No video stream found in file"); + return false; + } + + decoder_ = decoder_manager.acquire(type, input_ctx->streams[video_stream_idx_]->codecpar, !no_hw_decoder); if (!decoder_) { return false; } @@ -90,7 +96,9 @@ bool FrameReader::loadFromFile(CameraType type, const std::string &file, bool no AVPacket pkt; packets_info.reserve(60 * 20); // 20fps, one minute while (!(abort && *abort) && av_read_frame(input_ctx, &pkt) == 0) { - packets_info.emplace_back(PacketInfo{.flags = pkt.flags, .pos = pkt.pos}); + if (pkt.stream_index == video_stream_idx_) { + packets_info.emplace_back(PacketInfo{.flags = pkt.flags, .pos = pkt.pos}); + } av_packet_unref(&pkt); } avio_seek(input_ctx->pb, 0, SEEK_SET); @@ -168,17 +176,17 @@ bool VideoDecoder::initHardwareDecoder(AVHWDeviceType hw_device_type) { } bool VideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { - int from_idx = idx; + int current_idx = idx; if (idx != reader->prev_idx + 1) { // seeking to the nearest key frame for (int i = idx; i >= 0; --i) { if (reader->packets_info[i].flags & AV_PKT_FLAG_KEY) { - from_idx = i; + current_idx = i; break; } } - auto pos = reader->packets_info[from_idx].pos; + auto pos = reader->packets_info[current_idx].pos; int ret = avformat_seek_file(reader->input_ctx, 0, pos, pos, pos, AVSEEK_FLAG_BYTE); if (ret < 0) { rError("Failed to seek to byte position %lld: %d", pos, AVERROR(ret)); @@ -188,18 +196,27 @@ bool VideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { } reader->prev_idx = idx; - bool result = false; AVPacket pkt; - for (int i = from_idx; i <= idx; ++i) { - if (av_read_frame(reader->input_ctx, &pkt) == 0) { - AVFrame *f = decodeFrame(&pkt); - if (f && i == idx) { - result = copyBuffer(f, buf); - } + while (av_read_frame(reader->input_ctx, &pkt) >= 0) { + // Skip non-video packets + if (pkt.stream_index != reader->video_stream_idx_) { av_packet_unref(&pkt); + continue; + } + + AVFrame *frame = decodeFrame(&pkt); + av_packet_unref(&pkt); + if (!frame) { + rError("Failed to decode frame at index %d", current_idx); + return false; + } + + if (current_idx++ == idx) { + return copyBuffer(frame, buf); } } - return result; + rError("Failed to find frame at index %d", idx); + return false; } AVFrame *VideoDecoder::decodeFrame(AVPacket *pkt) { diff --git a/tools/replay/framereader.h b/tools/replay/framereader.h index 03ea5be8b2..a15847e311 100644 --- a/tools/replay/framereader.h +++ b/tools/replay/framereader.h @@ -28,6 +28,7 @@ public: VideoDecoder *decoder_ = nullptr; AVFormatContext *input_ctx = nullptr; + int video_stream_idx_ = -1; int prev_idx = -1; struct PacketInfo { int flags; diff --git a/tools/replay/lib/rp_helpers.py b/tools/replay/lib/rp_helpers.py deleted file mode 100644 index aa20ab8e32..0000000000 --- a/tools/replay/lib/rp_helpers.py +++ /dev/null @@ -1,109 +0,0 @@ -import numpy as np -from openpilot.selfdrive.controls.radard import RADAR_TO_CAMERA - -# Color palette used for rerun AnnotationContext -rerunColorPalette = [(96, "red", (255, 0, 0)), - (100, "pink", (255, 36, 0)), - (124, "yellow", (255, 255, 0)), - (230, "vibrantpink", (255, 36, 170)), - (240, "orange", (255, 146, 0)), - (255, "white", (255, 255, 255)), - (110, "carColor", (255,0,127)), - (0, "background", (0, 0, 0))] - - -class UIParams: - lidar_x, lidar_y, lidar_zoom = 384, 960, 6 - lidar_car_x, lidar_car_y = lidar_x / 2., lidar_y / 1.1 - car_hwidth = 1.7272 / 2 * lidar_zoom - car_front = 2.6924 * lidar_zoom - car_back = 1.8796 * lidar_zoom - car_color = rerunColorPalette[6][0] -UP = UIParams - - -def to_topdown_pt(y, x): - px, py = x * UP.lidar_zoom + UP.lidar_car_x, -y * UP.lidar_zoom + UP.lidar_car_y - if px > 0 and py > 0 and px < UP.lidar_x and py < UP.lidar_y: - return int(px), int(py) - return -1, -1 - - -def draw_path(path, lid_overlay, lid_color=None): - x, y = np.asarray(path.x), np.asarray(path.y) - # draw lidar path point on lidar - if lid_color is not None and lid_overlay is not None: - for i in range(len(x)): - px, py = to_topdown_pt(x[i], y[i]) - if px != -1: - lid_overlay[px, py] = lid_color - - -def plot_model(m, lid_overlay): - if lid_overlay is None: - return - for lead in m.leadsV3: - if lead.prob < 0.5: - continue - x, y = lead.x[0], lead.y[0] - x_std = lead.xStd[0] - x -= RADAR_TO_CAMERA - _, py_top = to_topdown_pt(x + x_std, y) - px, py_bottom = to_topdown_pt(x - x_std, y) - lid_overlay[int(round(px - 4)):int(round(px + 4)), py_top:py_bottom] = rerunColorPalette[2][0] - - for path in m.laneLines: - draw_path(path, lid_overlay, rerunColorPalette[2][0]) - for edge in m.roadEdges: - draw_path(edge, lid_overlay, rerunColorPalette[0][0]) - draw_path(m.position, lid_overlay, rerunColorPalette[0][0]) - - -def plot_lead(rs, lid_overlay): - for lead in [rs.leadOne, rs.leadTwo]: - if not lead.status: - continue - x = lead.dRel - px_left, py = to_topdown_pt(x, -10) - px_right, _ = to_topdown_pt(x, 10) - lid_overlay[px_left:px_right, py] = rerunColorPalette[0][0] - - -def update_radar_points(lt, lid_overlay): - ar_pts = [] - if lt is not None: - ar_pts = {} - for track in lt: - ar_pts[track.trackId] = [track.dRel, track.yRel, track.vRel, track.aRel, track.oncoming, track.stationary] - for ids, pt in ar_pts.items(): - # negative here since radar is left positive - px, py = to_topdown_pt(pt[0], -pt[1]) - if px != -1: - if pt[-1]: - color = rerunColorPalette[4][0] - elif pt[-2]: - color = rerunColorPalette[3][0] - else: - color = rerunColorPalette[5][0] - if int(ids) == 1: - lid_overlay[px - 2:px + 2, py - 10:py + 10] = rerunColorPalette[1][0] - else: - lid_overlay[px - 2:px + 2, py - 2:py + 2] = color - - -def get_blank_lid_overlay(UP): - lid_overlay = np.zeros((UP.lidar_x, UP.lidar_y), 'uint8') - # Draw the car. - lid_overlay[int(round(UP.lidar_car_x - UP.car_hwidth)):int( - round(UP.lidar_car_x + UP.car_hwidth)), int(round(UP.lidar_car_y - - UP.car_front))] = UP.car_color - lid_overlay[int(round(UP.lidar_car_x - UP.car_hwidth)):int( - round(UP.lidar_car_x + UP.car_hwidth)), int(round(UP.lidar_car_y + - UP.car_back))] = UP.car_color - lid_overlay[int(round(UP.lidar_car_x - UP.car_hwidth)), int( - round(UP.lidar_car_y - UP.car_front)):int(round( - UP.lidar_car_y + UP.car_back))] = UP.car_color - lid_overlay[int(round(UP.lidar_car_x + UP.car_hwidth)), int( - round(UP.lidar_car_y - UP.car_front)):int(round( - UP.lidar_car_y + UP.car_back))] = UP.car_color - return lid_overlay diff --git a/tools/replay/main.cc b/tools/replay/main.cc index 38a1da292a..f950985075 100644 --- a/tools/replay/main.cc +++ b/tools/replay/main.cc @@ -30,7 +30,7 @@ Options: --qcam Load qcamera --no-hw-decoder Disable HW video decoding --no-vipc Do not output video - --all Output all messages including uiDebug, userFlag + --all Output all messages including uiDebug, userBookmark -h, --help Show this help message )"; diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index a8e5cd9d43..7ecad82873 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -20,7 +20,7 @@ Replay::Replay(const std::string &route, std::vector allow, std::ve std::signal(SIGUSR1, interrupt_sleep_handler); if (!(flags_ & REPLAY_FLAG_ALL_SERVICES)) { - block.insert(block.end(), {"uiDebug", "userFlag"}); + block.insert(block.end(), {"uiDebug", "userBookmark"}); } setupServices(allow, block); setupSegmentManager(!allow.empty() || !block.empty()); diff --git a/tools/replay/rp_visualization.py b/tools/replay/rp_visualization.py deleted file mode 100755 index 25169b72ae..0000000000 --- a/tools/replay/rp_visualization.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import os -import sys -import numpy as np -import rerun as rr -import cereal.messaging as messaging -from openpilot.common.basedir import BASEDIR -from openpilot.tools.replay.lib.rp_helpers import (UP, rerunColorPalette, - get_blank_lid_overlay, - update_radar_points, plot_lead, - plot_model) -from msgq.visionipc import VisionIpcClient, VisionStreamType - -os.environ['BASEDIR'] = BASEDIR - -UP.lidar_zoom = 6 - -def visualize(addr): - sm = messaging.SubMaster(['radarState', 'liveTracks', 'modelV2'], addr=addr) - vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, True) - while True: - if not vipc_client.is_connected(): - vipc_client.connect(True) - new_data = vipc_client.recv() - if new_data is None or not new_data.data.any(): - continue - - sm.update(0) - lid_overlay = get_blank_lid_overlay(UP) - if sm.recv_frame['modelV2']: - plot_model(sm['modelV2'], lid_overlay) - if sm.recv_frame['radarState']: - plot_lead(sm['radarState'], lid_overlay) - liveTracksTime = sm.logMonoTime['liveTracks'] - if sm.updated['liveTracks']: - update_radar_points(sm['liveTracks'], lid_overlay) - rr.set_time_nanos("TIMELINE", liveTracksTime) - rr.log("tracks", rr.SegmentationImage(np.flip(np.rot90(lid_overlay, k=-1), axis=1))) - - -def get_arg_parser(): - parser = argparse.ArgumentParser( - description="Show replay data in a UI.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("ip_address", nargs="?", default="127.0.0.1", - help="The ip address on which to receive zmq messages.") - parser.add_argument("--frame-address", default=None, - help="The frame address (fully qualified ZMQ endpoint for frames) on which to receive zmq messages.") - return parser - - -if __name__ == "__main__": - args = get_arg_parser().parse_args(sys.argv[1:]) - if args.ip_address != "127.0.0.1": - os.environ["ZMQ"] = "1" - messaging.reset_context() - rr.init("RadarPoints", spawn= True) - rr.log("tracks", rr.AnnotationContext(rerunColorPalette), static=True) - visualize(args.ip_address) diff --git a/tools/replay/timeline.cc b/tools/replay/timeline.cc index a4c2ffb700..39ea8b1bed 100644 --- a/tools/replay/timeline.cc +++ b/tools/replay/timeline.cc @@ -26,7 +26,7 @@ std::optional Timeline::find(double cur_ts, FindFlag flag) const { return entry.end_time; } } else if (entry.start_time > cur_ts) { - if ((flag == FindFlag::nextUserFlag && entry.type == TimelineType::UserFlag) || + if ((flag == FindFlag::nextUserBookmark && entry.type == TimelineType::UserBookmark) || (flag == FindFlag::nextInfo && entry.type == TimelineType::AlertInfo) || (flag == FindFlag::nextWarning && entry.type == TimelineType::AlertWarning) || (flag == FindFlag::nextCritical && entry.type == TimelineType::AlertCritical)) { @@ -66,8 +66,8 @@ void Timeline::buildTimeline(const Route &route, uint64_t route_start_ts, bool l auto cs = reader.getRoot().getSelfdriveState(); updateEngagementStatus(cs, current_engaged_idx, seconds); updateAlertStatus(cs, current_alert_idx, seconds); - } else if (e.which == cereal::Event::Which::USER_FLAG) { - staging_entries_.emplace_back(Entry{seconds, seconds, TimelineType::UserFlag}); + } else if (e.which == cereal::Event::Which::USER_BOOKMARK) { + staging_entries_.emplace_back(Entry{seconds, seconds, TimelineType::UserBookmark}); } } diff --git a/tools/replay/timeline.h b/tools/replay/timeline.h index 74add9e5cc..9688b6b95e 100644 --- a/tools/replay/timeline.h +++ b/tools/replay/timeline.h @@ -7,8 +7,8 @@ #include "tools/replay/route.h" -enum class TimelineType { None, Engaged, AlertInfo, AlertWarning, AlertCritical, UserFlag }; -enum class FindFlag { nextEngagement, nextDisEngagement, nextUserFlag, nextInfo, nextWarning, nextCritical }; +enum class TimelineType { None, Engaged, AlertInfo, AlertWarning, AlertCritical, UserBookmark }; +enum class FindFlag { nextEngagement, nextDisEngagement, nextUserBookmark, nextInfo, nextWarning, nextCritical }; class Timeline { public: diff --git a/tools/replay/ui.py b/tools/replay/ui.py index 6e40579902..d71d63d1ce 100755 --- a/tools/replay/ui.py +++ b/tools/replay/ui.py @@ -149,7 +149,7 @@ def ui_thread(addr): plot_arr[-1, name_to_arr_idx['angle_steers']] = sm['carState'].steeringAngleDeg plot_arr[-1, name_to_arr_idx['angle_steers_des']] = sm['carControl'].actuators.steeringAngleDeg plot_arr[-1, name_to_arr_idx['angle_steers_k']] = angle_steers_k - plot_arr[-1, name_to_arr_idx['gas']] = sm['carState'].gas + plot_arr[-1, name_to_arr_idx['gas']] = sm['carState'].gasDEPRECATED # TODO gas is deprecated plot_arr[-1, name_to_arr_idx['computer_gas']] = np.clip(sm['carControl'].actuators.accel/4.0, 0.0, 1.0) plot_arr[-1, name_to_arr_idx['user_brake']] = sm['carState'].brake diff --git a/tools/replay/unlog_ci_segment.py b/tools/replay/unlog_ci_segment.py index 6ddc1b298a..e5a7a3ffde 100755 --- a/tools/replay/unlog_ci_segment.py +++ b/tools/replay/unlog_ci_segment.py @@ -42,7 +42,7 @@ def replay(route, segment, loop): msg = msgs[i].as_builder() next_msg = msgs[i + 1] - start_time = time.time() + start_time = time.monotonic() w = msg.which() if w == 'roadCameraState': @@ -63,7 +63,7 @@ def replay(route, segment, loop): socks[w] = None lag += (next_msg.logMonoTime - msg.logMonoTime) / 1e9 - lag -= time.time() - start_time + lag -= time.monotonic() - start_time dt = max(lag, 0.0) lag -= dt diff --git a/tools/rerun/README.md b/tools/rerun/README.md deleted file mode 100644 index 11a8d33ee1..0000000000 --- a/tools/rerun/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Rerun -Rerun is a tool to quickly visualize time series data. It supports all openpilot logs , both the `logMessages` and video logs. - -[Instructions](https://rerun.io/docs/reference/viewer/overview) for navigation within the Rerun Viewer. - -## Usage -``` -usage: run.py [-h] [--demo] [--qcam] [--fcam] [--ecam] [--dcam] [route_or_segment_name] - -A helper to run rerun on openpilot routes - -positional arguments: - route_or_segment_name - The route or segment name to plot (default: None) - -options: - -h, --help show this help message and exit - --demo Use the demo route instead of providing one (default: False) - --qcam Show low-res road camera (default: False) - --fcam Show driving camera (default: False) - --ecam Show wide camera (default: False) - --dcam Show driver monitoring camera (default: False) -``` - -Examples using route name to observe accelerometer and qcamera: - -`./run.py --qcam "a2a0ccea32023010/2023-07-27--13-01-19"` - -Examples using segment range (more on [SegmentRange](https://github.com/commaai/openpilot/tree/master/tools/lib)): - -`./run.py --qcam "a2a0ccea32023010/2023-07-27--13-01-19/2:4"` - -## Cautions: -- Showing hevc videos (`--fcam`, `--ecam`, and `--dcam`) are expensive, and it's recommended to use `--qcam` for optimized performance. If possible, limiting your route to a few segments using `SegmentRange` will speed up logging and reduce memory usage - -## Demo -`./run.py --qcam --demo` diff --git a/tools/rerun/camera_reader.py b/tools/rerun/camera_reader.py deleted file mode 100644 index 8366a3c1cf..0000000000 --- a/tools/rerun/camera_reader.py +++ /dev/null @@ -1,93 +0,0 @@ -import tqdm -import subprocess -import multiprocessing -from enum import StrEnum -from functools import partial -from collections import namedtuple - -from openpilot.tools.lib.framereader import ffprobe - -CameraConfig = namedtuple("CameraConfig", ["qcam", "fcam", "ecam", "dcam"]) - -class CameraType(StrEnum): - qcam = "qcamera" - fcam = "fcamera" - ecam = "ecamera" - dcam = "dcamera" - - -def probe_packet_info(camera_path): - args = ["ffprobe", "-v", "quiet", "-show_packets", "-probesize", "10M", camera_path] - dat = subprocess.check_output(args) - dat = dat.decode().split() - return dat - - -class _FrameReader: - def __init__(self, camera_path, segment, h, w, start_time): - self.camera_path = camera_path - self.segment = segment - self.h = h - self.w = w - self.start_time = start_time - - self.ts = self._get_ts() - - def _read_stream_nv12(self): - frame_sz = self.w * self.h * 3 // 2 - proc = subprocess.Popen( - ["ffmpeg", "-v", "quiet", "-i", self.camera_path, "-f", "rawvideo", "-pix_fmt", "nv12", "-"], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL - ) - try: - while True: - dat = proc.stdout.read(frame_sz) - if len(dat) == 0: - break - yield dat - finally: - proc.kill() - - def _get_ts(self): - dat = probe_packet_info(self.camera_path) - try: - ret = [float(d.split('=')[1]) for d in dat if d.startswith("pts_time=")] - except ValueError: - # pts_times aren't available. Infer timestamps from duration_times - ret = [d for d in dat if d.startswith("duration_time")] - ret = [float(d.split('=')[1])*(i+1)+(self.segment*60)+self.start_time for i, d in enumerate(ret)] - return ret - - def __iter__(self): - for i, frame in enumerate(self._read_stream_nv12()): - yield self.ts[i], frame - - -class CameraReader: - def __init__(self, camera_paths, start_time, seg_idxs): - self.seg_idxs = seg_idxs - self.camera_paths = camera_paths - self.start_time = start_time - - probe = ffprobe(camera_paths[0])["streams"][0] - self.h = probe["height"] - self.w = probe["width"] - - self.__frs = {} - - def _get_fr(self, i): - if i not in self.__frs: - self.__frs[i] = _FrameReader(self.camera_paths[i], segment=i, h=self.h, w=self.w, start_time=self.start_time) - return self.__frs[i] - - def _run_on_segment(self, func, i): - return func(self._get_fr(i)) - - def run_across_segments(self, num_processes, func, desc=None): - with multiprocessing.Pool(num_processes) as pool: - num_segs = len(self.seg_idxs) - for _ in tqdm.tqdm(pool.imap_unordered(partial(self._run_on_segment, func), self.seg_idxs), total=num_segs, desc=desc): - continue - diff --git a/tools/rerun/run.py b/tools/rerun/run.py deleted file mode 100755 index 6ce8a37937..0000000000 --- a/tools/rerun/run.py +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import argparse -import multiprocessing -import rerun as rr -import rerun.blueprint as rrb -from functools import partial -from collections import defaultdict - -from cereal.services import SERVICE_LIST -from openpilot.tools.rerun.camera_reader import probe_packet_info, CameraReader, CameraConfig, CameraType -from openpilot.tools.lib.logreader import LogReader -from openpilot.tools.lib.route import Route, SegmentRange - - -NUM_CPUS = multiprocessing.cpu_count() -DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" -RR_TIMELINE_NAME = "Timeline" -RR_WIN = "openpilot logs" - - -""" -Relevant upstream Rerun issues: -- loading videos directly: https://github.com/rerun-io/rerun/issues/6532 -""" - -class Rerunner: - def __init__(self, route, segment_range, camera_config): - self.lr = LogReader(route_or_segment_name) - - # hevc files don't have start_time. We get it from qcamera.ts - start_time = 0 - dat = probe_packet_info(route.qcamera_paths()[0]) - for d in dat: - if d.startswith("pts_time="): - start_time = float(d.split('=')[1]) - break - - qcam, fcam, ecam, dcam = camera_config - self.camera_readers = {} - if qcam: - self.camera_readers[CameraType.qcam] = CameraReader(route.qcamera_paths(), start_time, segment_range.seg_idxs) - if fcam: - self.camera_readers[CameraType.fcam] = CameraReader(route.camera_paths(), start_time, segment_range.seg_idxs) - if ecam: - self.camera_readers[CameraType.ecam] = CameraReader(route.ecamera_paths(), start_time, segment_range.seg_idxs) - if dcam: - self.camera_readers[CameraType.dcam] = CameraReader(route.dcamera_paths(), start_time, segment_range.seg_idxs) - - def _create_blueprint(self): - blueprint = None - service_views = [] - - for topic in sorted(SERVICE_LIST.keys()): - View = rrb.TimeSeriesView if topic != "thumbnail" else rrb.Spatial2DView - service_views.append(View(name=topic, origin=f"/{topic}/", visible=False)) - rr.log(topic, rr.SeriesLine(name=topic), timeless=True) - - center_view = [rrb.Vertical(*service_views, name="streams")] - if len(self.camera_readers): - center_view.append(rrb.Vertical(*[rrb.Spatial2DView(name=cam_type, origin=cam_type) for cam_type in self.camera_readers.keys()], name="cameras")) - - blueprint = rrb.Blueprint( - rrb.Horizontal( - *center_view - ), - rrb.SelectionPanel(expanded=False), - rrb.TimePanel(expanded=False), - ) - return blueprint - - @staticmethod - def _parse_msg(msg, parent_key=''): - stack = [(msg, parent_key)] - while stack: - current_msg, current_parent_key = stack.pop() - if isinstance(current_msg, list): - for index, item in enumerate(current_msg): - new_key = f"{current_parent_key}/{index}" - if isinstance(item, (int, float)): - yield new_key, item - elif isinstance(item, dict): - stack.append((item, new_key)) - elif isinstance(current_msg, dict): - for key, value in current_msg.items(): - new_key = f"{current_parent_key}/{key}" - if isinstance(value, (int, float)): - yield new_key, value - elif isinstance(value, dict): - stack.append((value, new_key)) - elif isinstance(value, list): - for index, item in enumerate(value): - if isinstance(item, (int, float)): - yield f"{new_key}/{index}", item - else: - pass # Not a plottable value - - @staticmethod - @rr.shutdown_at_exit - def _process_log_msgs(blueprint, lr): - rr.init(RR_WIN) - rr.connect() - rr.send_blueprint(blueprint) - - log_msgs = defaultdict(lambda: defaultdict(list)) - for msg in lr: - msg_type = msg.which() - - if msg_type == "thumbnail": - continue - - for entity_path, dat in Rerunner._parse_msg(msg.to_dict()[msg_type], msg_type): - log_msgs[entity_path]["times"].append(msg.logMonoTime) - log_msgs[entity_path]["data"].append(dat) - - for entity_path, log_msg in log_msgs.items(): - rr.send_columns( - entity_path, - times=[rr.TimeNanosColumn(RR_TIMELINE_NAME, log_msg["times"])], - components=[rr.components.ScalarBatch(log_msg["data"])] - ) - - return [] - - @staticmethod - @rr.shutdown_at_exit - def _process_cam_readers(blueprint, cam_type, h, w, fr): - rr.init(RR_WIN) - rr.connect() - rr.send_blueprint(blueprint) - - for ts, frame in fr: - rr.set_time_nanos(RR_TIMELINE_NAME, int(ts * 1e9)) - rr.log(cam_type, rr.Image(bytes=frame, width=w, height=h, pixel_format=rr.PixelFormat.NV12)) - - def load_data(self): - rr.init(RR_WIN, spawn=True) - - startup_blueprint = self._create_blueprint() - self.lr.run_across_segments(NUM_CPUS, partial(self._process_log_msgs, startup_blueprint), desc="Log messages") - for cam_type, cr in self.camera_readers.items(): - cr.run_across_segments(NUM_CPUS, partial(self._process_cam_readers, startup_blueprint, cam_type, cr.h, cr.w), desc=cam_type) - - rr.send_blueprint(self._create_blueprint()) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="A helper to run rerun on openpilot routes", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one") - parser.add_argument("--qcam", action="store_true", help="Show low-res road camera") - parser.add_argument("--fcam", action="store_true", help="Show driving camera") - parser.add_argument("--ecam", action="store_true", help="Show wide camera") - parser.add_argument("--dcam", action="store_true", help="Show driver monitoring camera") - parser.add_argument("route_or_segment_name", nargs='?', help="The route or segment name to plot") - args = parser.parse_args() - - if not args.demo and not args.route_or_segment_name: - parser.print_help() - sys.exit() - - camera_config = CameraConfig(args.qcam, args.fcam, args.ecam, args.dcam) - route_or_segment_name = DEMO_ROUTE if args.demo else args.route_or_segment_name.strip() - - sr = SegmentRange(route_or_segment_name) - r = Route(sr.route_name) - - hevc_requested = any(camera_config[1:]) - if len(sr.seg_idxs) > 1 and hevc_requested: - print("You're requesting more than 1 segment with hevc videos, " + \ - "please be aware that might take a lot of memory " + \ - "since rerun isn't yet well supported for high resolution video logging") - response = input("Do you wish to continue? (Y/n): ") - if response.strip().lower() != "y": - sys.exit() - - rerunner = Rerunner(r, sr, camera_config) - rerunner.load_data() - diff --git a/tools/scripts/ssh.py b/tools/scripts/ssh.py new file mode 100755 index 0000000000..0429799a2e --- /dev/null +++ b/tools/scripts/ssh.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +import os +import sys +import argparse +import re + +from openpilot.common.basedir import BASEDIR +from openpilot.tools.lib.auth_config import get_token +from openpilot.tools.lib.api import CommaApi + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="A helper for connecting to devices over the comma prime SSH proxy.\ + Adding your SSH key to your SSH config is recommended for more convenient use; see https://docs.comma.ai/how-to/connect-to-comma/.") + parser.add_argument("device", help="device name or dongle id") + parser.add_argument("--host", help="ssh jump server host", default="ssh.comma.ai") + parser.add_argument("--port", help="ssh jump server port", default=22, type=int) + parser.add_argument("--key", help="ssh key", default=os.path.join(BASEDIR, "system/hardware/tici/id_rsa")) + parser.add_argument("--debug", help="enable debug output", action="store_true") + args = parser.parse_args() + + r = CommaApi(get_token()).get("v1/me/devices") + devices = {x['dongle_id']: x['alias'] for x in r} + + if not re.match("[0-9a-zA-Z]{16}", args.device): + user_input = args.device.replace(" ", "").lower() + matches = { k: v for k, v in devices.items() if isinstance(v, str) and user_input in v.replace(" ", "").lower() } + if len(matches) == 1: + dongle_id = list(matches.keys())[0] + else: + print(f"failed to look up dongle id for \"{args.device}\"", file=sys.stderr) + if len(matches) > 1: + print("found multiple matches:", file=sys.stderr) + for k, v in matches.items(): + print(f" \"{v}\" ({k})", file=sys.stderr) + exit(1) + else: + dongle_id = args.device + + name = dongle_id + if dongle_id in devices: + name = f"{devices[dongle_id]} ({dongle_id})" + print(f"connecting to {name} through {args.host}:{args.port} ...") + + command = [ + "ssh", + "-i", args.key, + "-o", f"ProxyCommand=ssh -i {args.key} -W %h:%p -p %p %h@{args.host}", + "-p", str(args.port), + ] + if args.debug: + command += ["-v"] + command += [ + f"comma@{dongle_id}", + ] + if args.debug: + print(" ".join([f"'{c}'" if " " in c else c for c in command])) + os.execvp(command[0], command) diff --git a/tools/sim/lib/simulated_car.py b/tools/sim/lib/simulated_car.py index ad0f4de5d0..2681b26904 100644 --- a/tools/sim/lib/simulated_car.py +++ b/tools/sim/lib/simulated_car.py @@ -46,7 +46,7 @@ class SimulatedCar: msg.append(self.packer.make_can_msg("SCM_BUTTONS", 0, {"CRUISE_BUTTONS": simulator_state.cruise_button})) - msg.append(self.packer.make_can_msg("GEARBOX", 0, {"GEAR": 4, "GEAR_SHIFTER": 8})) + msg.append(self.packer.make_can_msg("GEARBOX_AUTO", 0, {"GEAR_SHIFTER": 4})) msg.append(self.packer.make_can_msg("GAS_PEDAL_2", 0, {})) msg.append(self.packer.make_can_msg("SEATBELT_STATUS", 0, {"SEATBELT_DRIVER_LATCHED": 1})) msg.append(self.packer.make_can_msg("STEER_STATUS", 0, {"STEER_TORQUE_SENSOR": simulator_state.user_torque})) @@ -56,7 +56,6 @@ class SimulatedCar: msg.append(self.packer.make_can_msg("STEER_MOTOR_TORQUE", 0, {})) msg.append(self.packer.make_can_msg("EPB_STATUS", 0, {})) msg.append(self.packer.make_can_msg("DOORS_STATUS", 0, {})) - msg.append(self.packer.make_can_msg("CRUISE_PARAMS", 0, {})) msg.append(self.packer.make_can_msg("CRUISE", 0, {})) msg.append(self.packer.make_can_msg("CRUISE_FAULT_STATUS", 0, {})) msg.append(self.packer.make_can_msg("SCM_FEEDBACK", 0, diff --git a/tools/sim/lib/simulated_sensors.py b/tools/sim/lib/simulated_sensors.py index e78f984736..a8374a00cd 100644 --- a/tools/sim/lib/simulated_sensors.py +++ b/tools/sim/lib/simulated_sensors.py @@ -53,7 +53,7 @@ class SimulatedSensors: for _ in range(10): dat = messaging.new_message('gpsLocationExternal', valid=True) dat.gpsLocationExternal = { - "unixTimestampMillis": int(time.time() * 1000), + "unixTimestampMillis": int(time.time() * 1000), # noqa: TID251 "flags": 1, # valid fix "horizontalAccuracy": 1.0, "verticalAccuracy": 1.0, @@ -109,7 +109,7 @@ class SimulatedSensors: self.camerad.cam_send_yuv_wide_road(yuv) def update(self, simulator_state: 'SimulatorState', world: 'World'): - now = time.time() + now = time.monotonic() self.send_imu_message(simulator_state) self.send_gps_message(simulator_state) diff --git a/uv.lock b/uv.lock index 05a6746369..83d8ea7b07 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,14 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11, <3.13" +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.12' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", +] [[package]] name = "aiohappyeyeballs" @@ -13,7 +21,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.12.14" +version = "3.12.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -24,42 +32,42 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e6/0b/e39ad954107ebf213a2325038a3e7a506be3d98e1435e1f82086eec4cde2/aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2", size = 7822921, upload-time = "2025-07-10T13:05:33.968Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/e1/8029b29316971c5fa89cec170274582619a01b3d82dd1036872acc9bc7e8/aiohttp-3.12.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f4552ff7b18bcec18b60a90c6982049cdb9dac1dba48cf00b97934a06ce2e597", size = 709960, upload-time = "2025-07-10T13:03:11.936Z" }, - { url = "https://files.pythonhosted.org/packages/96/bd/4f204cf1e282041f7b7e8155f846583b19149e0872752711d0da5e9cc023/aiohttp-3.12.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8283f42181ff6ccbcf25acaae4e8ab2ff7e92b3ca4a4ced73b2c12d8cd971393", size = 482235, upload-time = "2025-07-10T13:03:14.118Z" }, - { url = "https://files.pythonhosted.org/packages/d6/0f/2a580fcdd113fe2197a3b9df30230c7e85bb10bf56f7915457c60e9addd9/aiohttp-3.12.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:040afa180ea514495aaff7ad34ec3d27826eaa5d19812730fe9e529b04bb2179", size = 470501, upload-time = "2025-07-10T13:03:16.153Z" }, - { url = "https://files.pythonhosted.org/packages/38/78/2c1089f6adca90c3dd74915bafed6d6d8a87df5e3da74200f6b3a8b8906f/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b413c12f14c1149f0ffd890f4141a7471ba4b41234fe4fd4a0ff82b1dc299dbb", size = 1740696, upload-time = "2025-07-10T13:03:18.4Z" }, - { url = "https://files.pythonhosted.org/packages/4a/c8/ce6c7a34d9c589f007cfe064da2d943b3dee5aabc64eaecd21faf927ab11/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d6f607ce2e1a93315414e3d448b831238f1874b9968e1195b06efaa5c87e245", size = 1689365, upload-time = "2025-07-10T13:03:20.629Z" }, - { url = "https://files.pythonhosted.org/packages/18/10/431cd3d089de700756a56aa896faf3ea82bee39d22f89db7ddc957580308/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:565e70d03e924333004ed101599902bba09ebb14843c8ea39d657f037115201b", size = 1788157, upload-time = "2025-07-10T13:03:22.44Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b2/26f4524184e0f7ba46671c512d4b03022633bcf7d32fa0c6f1ef49d55800/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4699979560728b168d5ab63c668a093c9570af2c7a78ea24ca5212c6cdc2b641", size = 1827203, upload-time = "2025-07-10T13:03:24.628Z" }, - { url = "https://files.pythonhosted.org/packages/e0/30/aadcdf71b510a718e3d98a7bfeaea2396ac847f218b7e8edb241b09bd99a/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad5fdf6af93ec6c99bf800eba3af9a43d8bfd66dce920ac905c817ef4a712afe", size = 1729664, upload-time = "2025-07-10T13:03:26.412Z" }, - { url = "https://files.pythonhosted.org/packages/67/7f/7ccf11756ae498fdedc3d689a0c36ace8fc82f9d52d3517da24adf6e9a74/aiohttp-3.12.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ac76627c0b7ee0e80e871bde0d376a057916cb008a8f3ffc889570a838f5cc7", size = 1666741, upload-time = "2025-07-10T13:03:28.167Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4d/35ebc170b1856dd020c92376dbfe4297217625ef4004d56587024dc2289c/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:798204af1180885651b77bf03adc903743a86a39c7392c472891649610844635", size = 1715013, upload-time = "2025-07-10T13:03:30.018Z" }, - { url = "https://files.pythonhosted.org/packages/7b/24/46dc0380146f33e2e4aa088b92374b598f5bdcde1718c77e8d1a0094f1a4/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4f1205f97de92c37dd71cf2d5bcfb65fdaed3c255d246172cce729a8d849b4da", size = 1710172, upload-time = "2025-07-10T13:03:31.821Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0a/46599d7d19b64f4d0fe1b57bdf96a9a40b5c125f0ae0d8899bc22e91fdce/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:76ae6f1dd041f85065d9df77c6bc9c9703da9b5c018479d20262acc3df97d419", size = 1690355, upload-time = "2025-07-10T13:03:34.754Z" }, - { url = "https://files.pythonhosted.org/packages/08/86/b21b682e33d5ca317ef96bd21294984f72379454e689d7da584df1512a19/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a194ace7bc43ce765338ca2dfb5661489317db216ea7ea700b0332878b392cab", size = 1783958, upload-time = "2025-07-10T13:03:36.53Z" }, - { url = "https://files.pythonhosted.org/packages/4f/45/f639482530b1396c365f23c5e3b1ae51c9bc02ba2b2248ca0c855a730059/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:16260e8e03744a6fe3fcb05259eeab8e08342c4c33decf96a9dad9f1187275d0", size = 1804423, upload-time = "2025-07-10T13:03:38.504Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e5/39635a9e06eed1d73671bd4079a3caf9cf09a49df08490686f45a710b80e/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c779e5ebbf0e2e15334ea404fcce54009dc069210164a244d2eac8352a44b28", size = 1717479, upload-time = "2025-07-10T13:03:40.158Z" }, - { url = "https://files.pythonhosted.org/packages/51/e1/7f1c77515d369b7419c5b501196526dad3e72800946c0099594c1f0c20b4/aiohttp-3.12.14-cp311-cp311-win32.whl", hash = "sha256:a289f50bf1bd5be227376c067927f78079a7bdeccf8daa6a9e65c38bae14324b", size = 427907, upload-time = "2025-07-10T13:03:41.801Z" }, - { url = "https://files.pythonhosted.org/packages/06/24/a6bf915c85b7a5b07beba3d42b3282936b51e4578b64a51e8e875643c276/aiohttp-3.12.14-cp311-cp311-win_amd64.whl", hash = "sha256:0b8a69acaf06b17e9c54151a6c956339cf46db4ff72b3ac28516d0f7068f4ced", size = 452334, upload-time = "2025-07-10T13:03:43.485Z" }, - { url = "https://files.pythonhosted.org/packages/c3/0d/29026524e9336e33d9767a1e593ae2b24c2b8b09af7c2bd8193762f76b3e/aiohttp-3.12.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a0ecbb32fc3e69bc25efcda7d28d38e987d007096cbbeed04f14a6662d0eee22", size = 701055, upload-time = "2025-07-10T13:03:45.59Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b8/a5e8e583e6c8c1056f4b012b50a03c77a669c2e9bf012b7cf33d6bc4b141/aiohttp-3.12.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0400f0ca9bb3e0b02f6466421f253797f6384e9845820c8b05e976398ac1d81a", size = 475670, upload-time = "2025-07-10T13:03:47.249Z" }, - { url = "https://files.pythonhosted.org/packages/29/e8/5202890c9e81a4ec2c2808dd90ffe024952e72c061729e1d49917677952f/aiohttp-3.12.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a56809fed4c8a830b5cae18454b7464e1529dbf66f71c4772e3cfa9cbec0a1ff", size = 468513, upload-time = "2025-07-10T13:03:49.377Z" }, - { url = "https://files.pythonhosted.org/packages/23/e5/d11db8c23d8923d3484a27468a40737d50f05b05eebbb6288bafcb467356/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f2e373276e4755691a963e5d11756d093e346119f0627c2d6518208483fb6d", size = 1715309, upload-time = "2025-07-10T13:03:51.556Z" }, - { url = "https://files.pythonhosted.org/packages/53/44/af6879ca0eff7a16b1b650b7ea4a827301737a350a464239e58aa7c387ef/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ca39e433630e9a16281125ef57ece6817afd1d54c9f1bf32e901f38f16035869", size = 1697961, upload-time = "2025-07-10T13:03:53.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/94/18457f043399e1ec0e59ad8674c0372f925363059c276a45a1459e17f423/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c748b3f8b14c77720132b2510a7d9907a03c20ba80f469e58d5dfd90c079a1c", size = 1753055, upload-time = "2025-07-10T13:03:55.368Z" }, - { url = "https://files.pythonhosted.org/packages/26/d9/1d3744dc588fafb50ff8a6226d58f484a2242b5dd93d8038882f55474d41/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a568abe1b15ce69d4cc37e23020720423f0728e3cb1f9bcd3f53420ec3bfe7", size = 1799211, upload-time = "2025-07-10T13:03:57.216Z" }, - { url = "https://files.pythonhosted.org/packages/73/12/2530fb2b08773f717ab2d249ca7a982ac66e32187c62d49e2c86c9bba9b4/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9888e60c2c54eaf56704b17feb558c7ed6b7439bca1e07d4818ab878f2083660", size = 1718649, upload-time = "2025-07-10T13:03:59.469Z" }, - { url = "https://files.pythonhosted.org/packages/b9/34/8d6015a729f6571341a311061b578e8b8072ea3656b3d72329fa0faa2c7c/aiohttp-3.12.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3006a1dc579b9156de01e7916d38c63dc1ea0679b14627a37edf6151bc530088", size = 1634452, upload-time = "2025-07-10T13:04:01.698Z" }, - { url = "https://files.pythonhosted.org/packages/ff/4b/08b83ea02595a582447aeb0c1986792d0de35fe7a22fb2125d65091cbaf3/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa8ec5c15ab80e5501a26719eb48a55f3c567da45c6ea5bb78c52c036b2655c7", size = 1695511, upload-time = "2025-07-10T13:04:04.165Z" }, - { url = "https://files.pythonhosted.org/packages/b5/66/9c7c31037a063eec13ecf1976185c65d1394ded4a5120dd5965e3473cb21/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:39b94e50959aa07844c7fe2206b9f75d63cc3ad1c648aaa755aa257f6f2498a9", size = 1716967, upload-time = "2025-07-10T13:04:06.132Z" }, - { url = "https://files.pythonhosted.org/packages/ba/02/84406e0ad1acb0fb61fd617651ab6de760b2d6a31700904bc0b33bd0894d/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:04c11907492f416dad9885d503fbfc5dcb6768d90cad8639a771922d584609d3", size = 1657620, upload-time = "2025-07-10T13:04:07.944Z" }, - { url = "https://files.pythonhosted.org/packages/07/53/da018f4013a7a179017b9a274b46b9a12cbeb387570f116964f498a6f211/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:88167bd9ab69bb46cee91bd9761db6dfd45b6e76a0438c7e884c3f8160ff21eb", size = 1737179, upload-time = "2025-07-10T13:04:10.182Z" }, - { url = "https://files.pythonhosted.org/packages/49/e8/ca01c5ccfeaafb026d85fa4f43ceb23eb80ea9c1385688db0ef322c751e9/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:791504763f25e8f9f251e4688195e8b455f8820274320204f7eafc467e609425", size = 1765156, upload-time = "2025-07-10T13:04:12.029Z" }, - { url = "https://files.pythonhosted.org/packages/22/32/5501ab525a47ba23c20613e568174d6c63aa09e2caa22cded5c6ea8e3ada/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785b112346e435dd3a1a67f67713a3fe692d288542f1347ad255683f066d8e0", size = 1724766, upload-time = "2025-07-10T13:04:13.961Z" }, - { url = "https://files.pythonhosted.org/packages/06/af/28e24574801fcf1657945347ee10df3892311c2829b41232be6089e461e7/aiohttp-3.12.14-cp312-cp312-win32.whl", hash = "sha256:15f5f4792c9c999a31d8decf444e79fcfd98497bf98e94284bf390a7bb8c1729", size = 422641, upload-time = "2025-07-10T13:04:16.018Z" }, - { url = "https://files.pythonhosted.org/packages/98/d5/7ac2464aebd2eecac38dbe96148c9eb487679c512449ba5215d233755582/aiohttp-3.12.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b66e1a182879f579b105a80d5c4bd448b91a57e8933564bf41665064796a338", size = 449316, upload-time = "2025-07-10T13:04:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/20/19/9e86722ec8e835959bd97ce8c1efa78cf361fa4531fca372551abcc9cdd6/aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", size = 711246, upload-time = "2025-07-29T05:50:15.937Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/0a31fcb1a7d4629ac9d8f01f1cb9242e2f9943f47f5d03215af91c3c1a26/aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", size = 483515, upload-time = "2025-07-29T05:50:17.442Z" }, + { url = "https://files.pythonhosted.org/packages/62/6c/94846f576f1d11df0c2e41d3001000527c0fdf63fce7e69b3927a731325d/aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", size = 471776, upload-time = "2025-07-29T05:50:19.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6c/f766d0aaafcee0447fad0328da780d344489c042e25cd58fde566bf40aed/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5", size = 1741977, upload-time = "2025-07-29T05:50:21.665Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/fb779a05ba6ff44d7bc1e9d24c644e876bfff5abe5454f7b854cace1b9cc/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728", size = 1690645, upload-time = "2025-07-29T05:50:23.333Z" }, + { url = "https://files.pythonhosted.org/packages/37/4e/a22e799c2035f5d6a4ad2cf8e7c1d1bd0923192871dd6e367dafb158b14c/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16", size = 1789437, upload-time = "2025-07-29T05:50:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/28/e5/55a33b991f6433569babb56018b2fb8fb9146424f8b3a0c8ecca80556762/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0", size = 1828482, upload-time = "2025-07-29T05:50:26.693Z" }, + { url = "https://files.pythonhosted.org/packages/c6/82/1ddf0ea4f2f3afe79dffed5e8a246737cff6cbe781887a6a170299e33204/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b", size = 1730944, upload-time = "2025-07-29T05:50:28.382Z" }, + { url = "https://files.pythonhosted.org/packages/1b/96/784c785674117b4cb3877522a177ba1b5e4db9ce0fd519430b5de76eec90/aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd", size = 1668020, upload-time = "2025-07-29T05:50:30.032Z" }, + { url = "https://files.pythonhosted.org/packages/12/8a/8b75f203ea7e5c21c0920d84dd24a5c0e971fe1e9b9ebbf29ae7e8e39790/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8", size = 1716292, upload-time = "2025-07-29T05:50:31.983Z" }, + { url = "https://files.pythonhosted.org/packages/47/0b/a1451543475bb6b86a5cfc27861e52b14085ae232896a2654ff1231c0992/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50", size = 1711451, upload-time = "2025-07-29T05:50:33.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/fd/793a23a197cc2f0d29188805cfc93aa613407f07e5f9da5cd1366afd9d7c/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676", size = 1691634, upload-time = "2025-07-29T05:50:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bf/23a335a6670b5f5dfc6d268328e55a22651b440fca341a64fccf1eada0c6/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7", size = 1785238, upload-time = "2025-07-29T05:50:37.597Z" }, + { url = "https://files.pythonhosted.org/packages/57/4f/ed60a591839a9d85d40694aba5cef86dde9ee51ce6cca0bb30d6eb1581e7/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7", size = 1805701, upload-time = "2025-07-29T05:50:39.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/e0/444747a9455c5de188c0f4a0173ee701e2e325d4b2550e9af84abb20cdba/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685", size = 1718758, upload-time = "2025-07-29T05:50:41.292Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/1006278d1ffd13a698e5dd4bfa01e5878f6bddefc296c8b62649753ff249/aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b", size = 428868, upload-time = "2025-07-29T05:50:43.063Z" }, + { url = "https://files.pythonhosted.org/packages/10/97/ad2b18700708452400278039272032170246a1bf8ec5d832772372c71f1a/aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d", size = 453273, upload-time = "2025-07-29T05:50:44.613Z" }, + { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload-time = "2025-07-29T05:50:46.507Z" }, + { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload-time = "2025-07-29T05:50:48.067Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload-time = "2025-07-29T05:50:49.669Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload-time = "2025-07-29T05:50:51.368Z" }, + { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload-time = "2025-07-29T05:50:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload-time = "2025-07-29T05:50:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload-time = "2025-07-29T05:50:57.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload-time = "2025-07-29T05:50:59.192Z" }, + { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload-time = "2025-07-29T05:51:01.394Z" }, + { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload-time = "2025-07-29T05:51:03.657Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload-time = "2025-07-29T05:51:05.911Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload-time = "2025-07-29T05:51:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload-time = "2025-07-29T05:51:09.56Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload-time = "2025-07-29T05:51:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload-time = "2025-07-29T05:51:13.689Z" }, + { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload-time = "2025-07-29T05:51:15.452Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload-time = "2025-07-29T05:51:17.239Z" }, ] [[package]] @@ -158,7 +166,7 @@ wheels = [ [[package]] name = "azure-identity" -version = "1.23.1" +version = "1.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -167,9 +175,9 @@ dependencies = [ { name = "msal-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/29/1201ffbb6a57a16524dd91f3e741b4c828a70aaba436578bdcb3fbcb438c/azure_identity-1.23.1.tar.gz", hash = "sha256:226c1ef982a9f8d5dcf6e0f9ed35eaef2a4d971e7dd86317e9b9d52e70a035e4", size = 266185, upload-time = "2025-07-15T19:16:38.077Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/44/f3ee20bacb220b6b4a2b0a6cf7e742eecb383a5ccf604dd79ec27c286b7e/azure_identity-1.24.0.tar.gz", hash = "sha256:6c3a40b2a70af831e920b89e6421e8dcd4af78a0cb38b9642d86c67643d4930c", size = 271630, upload-time = "2025-08-07T22:27:36.258Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/b3/e2d7ab810eb68575a5c7569b03c0228b8f4ce927ffa6211471b526f270c9/azure_identity-1.23.1-py3-none-any.whl", hash = "sha256:7eed28baa0097a47e3fb53bd35a63b769e6b085bb3cb616dfce2b67f28a004a1", size = 186810, upload-time = "2025-07-15T19:16:40.184Z" }, + { url = "https://files.pythonhosted.org/packages/a9/74/17428cb429e8d52f6d0d69ed685f4760a545cb0156594963a9337b53b6c9/azure_identity-1.24.0-py3-none-any.whl", hash = "sha256:9e04997cde0ab02ed66422c74748548e620b7b29361c72ce622acab0267ff7c4", size = 187890, upload-time = "2025-08-07T22:27:38.033Z" }, ] [[package]] @@ -212,11 +220,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.7.14" +version = "2025.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] [[package]] @@ -255,37 +263,33 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.2" +version = "3.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] [[package]] @@ -471,7 +475,7 @@ version = "0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-xlib", marker = "sys_platform == 'linux'" }, - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/2f/3a/46ca34abf0725a754bc44ef474ad34aedcc3ea23b052d97b18b76715a6a9/EWMHlib-0.2-py3-none-any.whl", hash = "sha256:f5b07d8cfd4c7734462ee744c32d490f2f3233fa7ab354240069344208d2f6f5", size = 46657, upload-time = "2024-04-17T08:15:56.338Z" }, @@ -756,24 +760,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/6d/344a164d32d65d503ffe9201cd74cf13a020099dc446554d1e50b07f167b/libusb1-3.3.1-py3-none-win_amd64.whl", hash = "sha256:6e21b772d80d6487fbb55d3d2141218536db302da82f1983754e96c72781c102", size = 141080, upload-time = "2025-03-24T05:36:46.594Z" }, ] -[[package]] -name = "llvmlite" -version = "0.44.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload-time = "2025-01-20T11:14:41.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305, upload-time = "2025-01-20T11:12:53.936Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090, upload-time = "2025-01-20T11:12:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858, upload-time = "2025-01-20T11:13:07.623Z" }, - { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200, upload-time = "2025-01-20T11:13:20.058Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193, upload-time = "2025-01-20T11:13:26.976Z" }, - { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297, upload-time = "2025-01-20T11:13:32.57Z" }, - { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105, upload-time = "2025-01-20T11:13:38.744Z" }, - { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901, upload-time = "2025-01-20T11:13:46.711Z" }, - { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247, upload-time = "2025-01-20T11:13:56.159Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380, upload-time = "2025-01-20T11:14:02.442Z" }, -] - [[package]] name = "lxml" version = "6.0.0" @@ -851,7 +837,7 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.10.3" +version = "3.10.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, @@ -864,20 +850,25 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044, upload-time = "2025-07-31T18:09:33.805Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873, upload-time = "2025-05-08T19:09:53.857Z" }, - { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205, upload-time = "2025-05-08T19:09:55.684Z" }, - { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823, upload-time = "2025-05-08T19:09:57.442Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464, upload-time = "2025-05-08T19:09:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103, upload-time = "2025-05-08T19:10:03.208Z" }, - { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492, upload-time = "2025-05-08T19:10:05.271Z" }, - { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689, upload-time = "2025-05-08T19:10:07.602Z" }, - { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466, upload-time = "2025-05-08T19:10:09.383Z" }, - { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252, upload-time = "2025-05-08T19:10:11.958Z" }, - { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321, upload-time = "2025-05-08T19:10:14.47Z" }, - { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972, upload-time = "2025-05-08T19:10:16.569Z" }, - { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954, upload-time = "2025-05-08T19:10:18.663Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c7/1f2db90a1d43710478bb1e9b57b162852f79234d28e4f48a28cc415aa583/matplotlib-3.10.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf", size = 8239216, upload-time = "2025-07-31T18:07:51.947Z" }, + { url = "https://files.pythonhosted.org/packages/82/6d/ca6844c77a4f89b1c9e4d481c412e1d1dbabf2aae2cbc5aa2da4a1d6683e/matplotlib-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf", size = 8102130, upload-time = "2025-07-31T18:07:53.65Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1e/5e187a30cc673a3e384f3723e5f3c416033c1d8d5da414f82e4e731128ea/matplotlib-3.10.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a", size = 8666471, upload-time = "2025-07-31T18:07:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/03/c0/95540d584d7d645324db99a845ac194e915ef75011a0d5e19e1b5cee7e69/matplotlib-3.10.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b4984d5064a35b6f66d2c11d668565f4389b1119cc64db7a4c1725bc11adffc", size = 9500518, upload-time = "2025-07-31T18:07:57.199Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2e/e019352099ea58b4169adb9c6e1a2ad0c568c6377c2b677ee1f06de2adc7/matplotlib-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3967424121d3a46705c9fa9bdb0931de3228f13f73d7bb03c999c88343a89d89", size = 9552372, upload-time = "2025-07-31T18:07:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/b7/81/3200b792a5e8b354f31f4101ad7834743ad07b6d620259f2059317b25e4d/matplotlib-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:33775bbeb75528555a15ac29396940128ef5613cf9a2d31fb1bfd18b3c0c0903", size = 8100634, upload-time = "2025-07-31T18:08:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/52/46/a944f6f0c1f5476a0adfa501969d229ce5ae60cf9a663be0e70361381f89/matplotlib-3.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:c61333a8e5e6240e73769d5826b9a31d8b22df76c0778f8480baf1b4b01c9420", size = 7978880, upload-time = "2025-07-31T18:08:03.407Z" }, + { url = "https://files.pythonhosted.org/packages/66/1e/c6f6bcd882d589410b475ca1fc22e34e34c82adff519caf18f3e6dd9d682/matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2", size = 8253056, upload-time = "2025-07-31T18:08:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/53/e6/d6f7d1b59413f233793dda14419776f5f443bcccb2dfc84b09f09fe05dbe/matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389", size = 8110131, upload-time = "2025-07-31T18:08:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/66/2b/bed8a45e74957549197a2ac2e1259671cd80b55ed9e1fe2b5c94d88a9202/matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea", size = 8669603, upload-time = "2025-07-31T18:08:09.064Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a7/315e9435b10d057f5e52dfc603cd353167ae28bb1a4e033d41540c0067a4/matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468", size = 9508127, upload-time = "2025-07-31T18:08:10.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d9/edcbb1f02ca99165365d2768d517898c22c6040187e2ae2ce7294437c413/matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369", size = 9566926, upload-time = "2025-07-31T18:08:13.186Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d9/6dd924ad5616c97b7308e6320cf392c466237a82a2040381163b7500510a/matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b", size = 8107599, upload-time = "2025-07-31T18:08:15.116Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f3/522dc319a50f7b0279fbe74f86f7a3506ce414bc23172098e8d2bdf21894/matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2", size = 7978173, upload-time = "2025-07-31T18:08:21.518Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d6/e921be4e1a5f7aca5194e1f016cb67ec294548e530013251f630713e456d/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f", size = 8233224, upload-time = "2025-07-31T18:09:27.512Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/a2b9b04824b9c349c8f1b2d21d5af43fa7010039427f2b133a034cb09e59/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b", size = 8098539, upload-time = "2025-07-31T18:09:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/fc/66/cd29ebc7f6c0d2a15d216fb572573e8fc38bd5d6dec3bd9d7d904c0949f7/matplotlib-3.10.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61", size = 8672192, upload-time = "2025-07-31T18:09:31.407Z" }, ] [[package]] @@ -1074,28 +1065,28 @@ wheels = [ [[package]] name = "mypy" -version = "1.17.0" +version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/82efb502b0b0f661c49aa21cfe3e1999ddf64bf5500fc03b5a1536a39d39/mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be", size = 10914150, upload-time = "2025-07-14T20:31:51.985Z" }, - { url = "https://files.pythonhosted.org/packages/03/96/8ef9a6ff8cedadff4400e2254689ca1dc4b420b92c55255b44573de10c54/mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61", size = 10039845, upload-time = "2025-07-14T20:32:30.527Z" }, - { url = "https://files.pythonhosted.org/packages/df/32/7ce359a56be779d38021d07941cfbb099b41411d72d827230a36203dbb81/mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f", size = 11837246, upload-time = "2025-07-14T20:32:01.28Z" }, - { url = "https://files.pythonhosted.org/packages/82/16/b775047054de4d8dbd668df9137707e54b07fe18c7923839cd1e524bf756/mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d", size = 12571106, upload-time = "2025-07-14T20:34:26.942Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cf/fa33eaf29a606102c8d9ffa45a386a04c2203d9ad18bf4eef3e20c43ebc8/mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3", size = 12759960, upload-time = "2025-07-14T20:33:42.882Z" }, - { url = "https://files.pythonhosted.org/packages/94/75/3f5a29209f27e739ca57e6350bc6b783a38c7621bdf9cac3ab8a08665801/mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70", size = 9503888, upload-time = "2025-07-14T20:32:34.392Z" }, - { url = "https://files.pythonhosted.org/packages/12/e9/e6824ed620bbf51d3bf4d6cbbe4953e83eaf31a448d1b3cfb3620ccb641c/mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb", size = 11086395, upload-time = "2025-07-14T20:34:11.452Z" }, - { url = "https://files.pythonhosted.org/packages/ba/51/a4afd1ae279707953be175d303f04a5a7bd7e28dc62463ad29c1c857927e/mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d", size = 10120052, upload-time = "2025-07-14T20:33:09.897Z" }, - { url = "https://files.pythonhosted.org/packages/8a/71/19adfeac926ba8205f1d1466d0d360d07b46486bf64360c54cb5a2bd86a8/mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8", size = 11861806, upload-time = "2025-07-14T20:32:16.028Z" }, - { url = "https://files.pythonhosted.org/packages/0b/64/d6120eca3835baf7179e6797a0b61d6c47e0bc2324b1f6819d8428d5b9ba/mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e", size = 12744371, upload-time = "2025-07-14T20:33:33.503Z" }, - { url = "https://files.pythonhosted.org/packages/1f/dc/56f53b5255a166f5bd0f137eed960e5065f2744509dfe69474ff0ba772a5/mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8", size = 12914558, upload-time = "2025-07-14T20:33:56.961Z" }, - { url = "https://files.pythonhosted.org/packages/69/ac/070bad311171badc9add2910e7f89271695a25c136de24bbafc7eded56d5/mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d", size = 9585447, upload-time = "2025-07-14T20:32:20.594Z" }, - { url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" }, + { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, + { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, + { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, + { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, + { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, + { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, ] [[package]] @@ -1118,30 +1109,39 @@ wheels = [ [[package]] name = "numpy" -version = "2.1.3" +version = "2.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090, upload-time = "2024-11-02T17:48:55.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/81/c8167192eba5247593cd9d305ac236847c2912ff39e11402e72ae28a4985/numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d", size = 21156252, upload-time = "2024-11-02T17:34:01.372Z" }, - { url = "https://files.pythonhosted.org/packages/da/74/5a60003fc3d8a718d830b08b654d0eea2d2db0806bab8f3c2aca7e18e010/numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41", size = 13784119, upload-time = "2024-11-02T17:34:23.809Z" }, - { url = "https://files.pythonhosted.org/packages/47/7c/864cb966b96fce5e63fcf25e1e4d957fe5725a635e5f11fe03f39dd9d6b5/numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9", size = 5352978, upload-time = "2024-11-02T17:34:34.001Z" }, - { url = "https://files.pythonhosted.org/packages/09/ac/61d07930a4993dd9691a6432de16d93bbe6aa4b1c12a5e573d468eefc1ca/numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09", size = 6892570, upload-time = "2024-11-02T17:34:45.401Z" }, - { url = "https://files.pythonhosted.org/packages/27/2f/21b94664f23af2bb52030653697c685022119e0dc93d6097c3cb45bce5f9/numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a", size = 13896715, upload-time = "2024-11-02T17:35:06.564Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f0/80811e836484262b236c684a75dfc4ba0424bc670e765afaa911468d9f39/numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b", size = 16339644, upload-time = "2024-11-02T17:35:30.888Z" }, - { url = "https://files.pythonhosted.org/packages/fa/81/ce213159a1ed8eb7d88a2a6ef4fbdb9e4ffd0c76b866c350eb4e3c37e640/numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee", size = 16712217, upload-time = "2024-11-02T17:35:56.703Z" }, - { url = "https://files.pythonhosted.org/packages/7d/84/4de0b87d5a72f45556b2a8ee9fc8801e8518ec867fc68260c1f5dcb3903f/numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0", size = 14399053, upload-time = "2024-11-02T17:36:22.3Z" }, - { url = "https://files.pythonhosted.org/packages/7e/1c/e5fabb9ad849f9d798b44458fd12a318d27592d4bc1448e269dec070ff04/numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9", size = 6534741, upload-time = "2024-11-02T17:36:33.552Z" }, - { url = "https://files.pythonhosted.org/packages/1e/48/a9a4b538e28f854bfb62e1dea3c8fea12e90216a276c7777ae5345ff29a7/numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2", size = 12869487, upload-time = "2024-11-02T17:36:52.909Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f0/385eb9970309643cbca4fc6eebc8bb16e560de129c91258dfaa18498da8b/numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", size = 20849658, upload-time = "2024-11-02T17:37:23.919Z" }, - { url = "https://files.pythonhosted.org/packages/54/4a/765b4607f0fecbb239638d610d04ec0a0ded9b4951c56dc68cef79026abf/numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", size = 13492258, upload-time = "2024-11-02T17:37:45.252Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a7/2332679479c70b68dccbf4a8eb9c9b5ee383164b161bee9284ac141fbd33/numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", size = 5090249, upload-time = "2024-11-02T17:37:54.252Z" }, - { url = "https://files.pythonhosted.org/packages/c1/67/4aa00316b3b981a822c7a239d3a8135be2a6945d1fd11d0efb25d361711a/numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", size = 6621704, upload-time = "2024-11-02T17:38:05.127Z" }, - { url = "https://files.pythonhosted.org/packages/5e/da/1a429ae58b3b6c364eeec93bf044c532f2ff7b48a52e41050896cf15d5b1/numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", size = 13606089, upload-time = "2024-11-02T17:38:25.997Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3e/3757f304c704f2f0294a6b8340fcf2be244038be07da4cccf390fa678a9f/numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", size = 16043185, upload-time = "2024-11-02T17:38:51.07Z" }, - { url = "https://files.pythonhosted.org/packages/43/97/75329c28fea3113d00c8d2daf9bc5828d58d78ed661d8e05e234f86f0f6d/numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", size = 16410751, upload-time = "2024-11-02T17:39:15.801Z" }, - { url = "https://files.pythonhosted.org/packages/ad/7a/442965e98b34e0ae9da319f075b387bcb9a1e0658276cc63adb8c9686f7b/numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", size = 14082705, upload-time = "2024-11-02T17:39:38.274Z" }, - { url = "https://files.pythonhosted.org/packages/ac/b6/26108cf2cfa5c7e03fb969b595c93131eab4a399762b51ce9ebec2332e80/numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", size = 6239077, upload-time = "2024-11-02T17:39:49.299Z" }, - { url = "https://files.pythonhosted.org/packages/a6/84/fa11dad3404b7634aaab50733581ce11e5350383311ea7a7010f464c0170/numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", size = 12566858, upload-time = "2024-11-02T17:40:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/96/26/1320083986108998bd487e2931eed2aeedf914b6e8905431487543ec911d/numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9", size = 21259016, upload-time = "2025-07-24T20:24:35.214Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2b/792b341463fa93fc7e55abbdbe87dac316c5b8cb5e94fb7a59fb6fa0cda5/numpy-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a0e27186e781a69959d0230dd9909b5e26024f8da10683bd6344baea1885168", size = 14451158, upload-time = "2025-07-24T20:24:58.397Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/e792d7209261afb0c9f4759ffef6135b35c77c6349a151f488f531d13595/numpy-2.3.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f0a1a8476ad77a228e41619af2fa9505cf69df928e9aaa165746584ea17fed2b", size = 5379817, upload-time = "2025-07-24T20:25:07.746Z" }, + { url = "https://files.pythonhosted.org/packages/49/ce/055274fcba4107c022b2113a213c7287346563f48d62e8d2a5176ad93217/numpy-2.3.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cbc95b3813920145032412f7e33d12080f11dc776262df1712e1638207dde9e8", size = 6913606, upload-time = "2025-07-24T20:25:18.84Z" }, + { url = "https://files.pythonhosted.org/packages/17/f2/e4d72e6bc5ff01e2ab613dc198d560714971900c03674b41947e38606502/numpy-2.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75018be4980a7324edc5930fe39aa391d5734531b1926968605416ff58c332d", size = 14589652, upload-time = "2025-07-24T20:25:40.356Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b0/fbeee3000a51ebf7222016e2939b5c5ecf8000a19555d04a18f1e02521b8/numpy-2.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b8200721840f5621b7bd03f8dcd78de33ec522fc40dc2641aa09537df010c3", size = 16938816, upload-time = "2025-07-24T20:26:05.721Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ec/2f6c45c3484cc159621ea8fc000ac5a86f1575f090cac78ac27193ce82cd/numpy-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f91e5c028504660d606340a084db4b216567ded1056ea2b4be4f9d10b67197f", size = 16370512, upload-time = "2025-07-24T20:26:30.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/01/dd67cf511850bd7aefd6347aaae0956ed415abea741ae107834aae7d6d4e/numpy-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fb1752a3bb9a3ad2d6b090b88a9a0ae1cd6f004ef95f75825e2f382c183b2097", size = 18884947, upload-time = "2025-07-24T20:26:58.24Z" }, + { url = "https://files.pythonhosted.org/packages/a7/17/2cf60fd3e6a61d006778735edf67a222787a8c1a7842aed43ef96d777446/numpy-2.3.2-cp311-cp311-win32.whl", hash = "sha256:4ae6863868aaee2f57503c7a5052b3a2807cf7a3914475e637a0ecd366ced220", size = 6599494, upload-time = "2025-07-24T20:27:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/d5/03/0eade211c504bda872a594f045f98ddcc6caef2b7c63610946845e304d3f/numpy-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:240259d6564f1c65424bcd10f435145a7644a65a6811cfc3201c4a429ba79170", size = 13087889, upload-time = "2025-07-24T20:27:29.558Z" }, + { url = "https://files.pythonhosted.org/packages/13/32/2c7979d39dafb2a25087e12310fc7f3b9d3c7d960df4f4bc97955ae0ce1d/numpy-2.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:4209f874d45f921bde2cff1ffcd8a3695f545ad2ffbef6d3d3c6768162efab89", size = 10459560, upload-time = "2025-07-24T20:27:46.803Z" }, + { url = "https://files.pythonhosted.org/packages/00/6d/745dd1c1c5c284d17725e5c802ca4d45cfc6803519d777f087b71c9f4069/numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b", size = 20956420, upload-time = "2025-07-24T20:28:18.002Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/e7b533ea5740641dd62b07a790af5d9d8fec36000b8e2d0472bd7574105f/numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f", size = 14184660, upload-time = "2025-07-24T20:28:39.522Z" }, + { url = "https://files.pythonhosted.org/packages/2b/53/102c6122db45a62aa20d1b18c9986f67e6b97e0d6fbc1ae13e3e4c84430c/numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0", size = 5113382, upload-time = "2025-07-24T20:28:48.544Z" }, + { url = "https://files.pythonhosted.org/packages/2b/21/376257efcbf63e624250717e82b4fae93d60178f09eb03ed766dbb48ec9c/numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b", size = 6647258, upload-time = "2025-07-24T20:28:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370", size = 14281409, upload-time = "2025-07-24T20:40:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73", size = 16641317, upload-time = "2025-07-24T20:40:56.625Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a7/af813a7b4f9a42f498dde8a4c6fcbff8100eed00182cc91dbaf095645f38/numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc", size = 16056262, upload-time = "2025-07-24T20:41:20.797Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5d/41c4ef8404caaa7f05ed1cfb06afe16a25895260eacbd29b4d84dff2920b/numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be", size = 18579342, upload-time = "2025-07-24T20:41:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/9950e44c5a11636f4a3af6e825ec23003475cc9a466edb7a759ed3ea63bd/numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036", size = 6320610, upload-time = "2025-07-24T20:42:01.551Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2f/244643a5ce54a94f0a9a2ab578189c061e4a87c002e037b0829dd77293b6/numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f", size = 12786292, upload-time = "2025-07-24T20:42:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/54/cd/7b5f49d5d78db7badab22d8323c1b6ae458fbf86c4fdfa194ab3cd4eb39b/numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07", size = 10194071, upload-time = "2025-07-24T20:42:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ea/50ebc91d28b275b23b7128ef25c3d08152bc4068f42742867e07a870a42a/numpy-2.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:14a91ebac98813a49bc6aa1a0dfc09513dcec1d97eaf31ca21a87221a1cdcb15", size = 21130338, upload-time = "2025-07-24T20:57:54.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/cdd5eac00dd5f137277355c318a955c0d8fb8aa486020c22afd305f8b88f/numpy-2.3.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:71669b5daae692189540cffc4c439468d35a3f84f0c88b078ecd94337f6cb0ec", size = 14375776, upload-time = "2025-07-24T20:58:16.303Z" }, + { url = "https://files.pythonhosted.org/packages/83/85/27280c7f34fcd305c2209c0cdca4d70775e4859a9eaa92f850087f8dea50/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:69779198d9caee6e547adb933941ed7520f896fd9656834c300bdf4dd8642712", size = 5304882, upload-time = "2025-07-24T20:58:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/b4/6500b24d278e15dd796f43824e69939d00981d37d9779e32499e823aa0aa/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2c3271cc4097beb5a60f010bcc1cc204b300bb3eafb4399376418a83a1c6373c", size = 6818405, upload-time = "2025-07-24T20:58:37.341Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c9/142c1e03f199d202da8e980c2496213509291b6024fd2735ad28ae7065c7/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8446acd11fe3dc1830568c941d44449fd5cb83068e5c70bd5a470d323d448296", size = 14419651, upload-time = "2025-07-24T20:58:59.048Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8023e87cbea31a750a6c00ff9427d65ebc5fef104a136bfa69f76266d614/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa098a5ab53fa407fded5870865c6275a5cd4101cfdef8d6fafc48286a96e981", size = 16760166, upload-time = "2025-07-24T21:28:56.38Z" }, + { url = "https://files.pythonhosted.org/packages/78/e3/6690b3f85a05506733c7e90b577e4762517404ea78bab2ca3a5cb1aeb78d/numpy-2.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6936aff90dda378c09bea075af0d9c675fe3a977a9d2402f95a87f440f59f619", size = 12977811, upload-time = "2025-07-24T21:29:18.234Z" }, ] [[package]] @@ -1171,19 +1171,19 @@ wheels = [ [[package]] name = "opencv-python-headless" -version = "4.12.0.88" +version = "4.11.0.86" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/63/6861102ec149c3cd298f4d1ea7ce9d6adbc7529221606ff1dab991a19adb/opencv-python-headless-4.12.0.88.tar.gz", hash = "sha256:cfdc017ddf2e59b6c2f53bc12d74b6b0be7ded4ec59083ea70763921af2b6c09", size = 95379675, upload-time = "2025-07-07T09:21:06.815Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929, upload-time = "2025-01-16T13:53:40.22Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/7d/414e243c5c8216a5277afd104a319cc1291c5e23f5eeef512db5629ee7f4/opencv_python_headless-4.12.0.88-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1e58d664809b3350c1123484dd441e1667cd7bed3086db1b9ea1b6f6cb20b50e", size = 37877864, upload-time = "2025-07-07T09:14:41.693Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/7e162714beed1cd5e7b5eb66fcbcba2f065c51b1d9da2463024c84d2f7c0/opencv_python_headless-4.12.0.88-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:365bb2e486b50feffc2d07a405b953a8f3e8eaa63865bc650034e5c71e7a5154", size = 57326608, upload-time = "2025-07-07T09:14:51.885Z" }, - { url = "https://files.pythonhosted.org/packages/69/4e/116720df7f1f7f3b59abc608ca30fbec9d2b3ae810afe4e4d26483d9dfa0/opencv_python_headless-4.12.0.88-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:aeb4b13ecb8b4a0beb2668ea07928160ea7c2cd2d9b5ef571bbee6bafe9cc8d0", size = 33145800, upload-time = "2025-07-07T09:15:00.367Z" }, - { url = "https://files.pythonhosted.org/packages/89/53/e19c21e0c4eb1275c3e2c97b081103b6dfb3938172264d283a519bf728b9/opencv_python_headless-4.12.0.88-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:236c8df54a90f4d02076e6f9c1cc763d794542e886c576a6fee46ec8ff75a7a9", size = 54023419, upload-time = "2025-07-07T09:15:10.164Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9c/a76fd5414de6ec9f21f763a600058a0c3e290053cea87e0275692b1375c0/opencv_python_headless-4.12.0.88-cp37-abi3-win32.whl", hash = "sha256:fde2cf5c51e4def5f2132d78e0c08f9c14783cd67356922182c6845b9af87dbd", size = 30225230, upload-time = "2025-07-07T09:15:17.045Z" }, - { url = "https://files.pythonhosted.org/packages/f2/35/0858e9e71b36948eafbc5e835874b63e515179dc3b742cbe3d76bc683439/opencv_python_headless-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:86b413bdd6c6bf497832e346cd5371995de148e579b9774f8eba686dee3f5528", size = 38923559, upload-time = "2025-07-07T09:15:25.229Z" }, + { url = "https://files.pythonhosted.org/packages/dc/53/2c50afa0b1e05ecdb4603818e85f7d174e683d874ef63a6abe3ac92220c8/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca", size = 37326460, upload-time = "2025-01-16T13:52:57.015Z" }, + { url = "https://files.pythonhosted.org/packages/3b/43/68555327df94bb9b59a1fd645f63fafb0762515344d2046698762fc19d58/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81", size = 56723330, upload-time = "2025-01-16T13:55:45.731Z" }, + { url = "https://files.pythonhosted.org/packages/45/be/1438ce43ebe65317344a87e4b150865c5585f4c0db880a34cdae5ac46881/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb", size = 29487060, upload-time = "2025-01-16T13:51:59.625Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5c/c139a7876099916879609372bfa513b7f1257f7f1a908b0bdc1c2328241b/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b", size = 49969856, upload-time = "2025-01-16T13:53:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/95/dd/ed1191c9dc91abcc9f752b499b7928aacabf10567bb2c2535944d848af18/opencv_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b", size = 29324425, upload-time = "2025-01-16T13:52:49.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/8a/69176a64335aed183529207ba8bc3d329c2999d852b4f3818027203f50e6/opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca", size = 39402386, upload-time = "2025-01-16T13:52:56.418Z" }, ] [[package]] @@ -1201,7 +1201,6 @@ dependencies = [ { name = "inputs" }, { name = "json-rpc" }, { name = "libusb1" }, - { name = "llvmlite" }, { name = "numpy" }, { name = "onnx" }, { name = "psutil" }, @@ -1272,7 +1271,6 @@ testing = [ ] tools = [ { name = "metadrive-simulator", marker = "platform_machine != 'aarch64'" }, - { name = "rerun-sdk" }, ] [package.metadata] @@ -1295,13 +1293,12 @@ requires-dist = [ { name = "jinja2", marker = "extra == 'docs'" }, { name = "json-rpc" }, { name = "libusb1" }, - { name = "llvmlite" }, { name = "matplotlib", marker = "extra == 'dev'" }, { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl" }, { name = "mkdocs", marker = "extra == 'docs'" }, { name = "mypy", marker = "extra == 'testing'" }, { name = "natsort", marker = "extra == 'docs'" }, - { name = "numpy", specifier = ">=2.0,<2.2" }, + { name = "numpy", specifier = ">=2.0" }, { name = "onnx", specifier = ">=1.14.0" }, { name = "opencv-python-headless", marker = "extra == 'dev'" }, { name = "parameterized", marker = "extra == 'dev'", specifier = ">=0.8,<0.9" }, @@ -1325,14 +1322,13 @@ requires-dist = [ { name = "pytest-repeat", marker = "extra == 'testing'" }, { name = "pytest-subtests", marker = "extra == 'testing'" }, { name = "pytest-timeout", marker = "extra == 'testing'" }, - { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=909e97b49d12401c10608f9d777bfc9dab8a4413" }, + { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }, { name = "pytools", marker = "platform_machine != 'aarch64' and extra == 'dev'", specifier = "<2024.1.11" }, { name = "pywinctl", marker = "extra == 'dev'" }, { name = "pyzmq" }, { name = "qrcode" }, { name = "raylib", marker = "extra == 'dev'" }, { name = "requests" }, - { name = "rerun-sdk", marker = "extra == 'tools'", specifier = ">=0.18" }, { name = "ruff", marker = "extra == 'testing'" }, { name = "scons" }, { name = "sentry-sdk" }, @@ -1480,14 +1476,14 @@ wheels = [ [[package]] name = "pre-commit-hooks" -version = "5.0.0" +version = "6.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruamel-yaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/7d/3299241a753c738d114600c360d754550b28c285281dc6a5132c4ccfae65/pre_commit_hooks-5.0.0.tar.gz", hash = "sha256:10626959a9eaf602fbfc22bc61b6e75801436f82326bfcee82bb1f2fc4bc646e", size = 29747, upload-time = "2024-10-05T18:43:11.225Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/4d/93e63e48f8fd16d6c1e4cef5dabadcade4d1325c7fd6f29f075a4d2284f3/pre_commit_hooks-6.0.0.tar.gz", hash = "sha256:76d8370c006f5026cdd638a397a678d26dda735a3c88137e05885a020f824034", size = 28293, upload-time = "2025-08-09T19:25:04.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/29/db1d855a661c02dbde5cab3057969133fcc62e7a0c6393e48fe9d0e81679/pre_commit_hooks-5.0.0-py2.py3-none-any.whl", hash = "sha256:8d71cfb582c5c314a5498d94e0104b6567a8b93fb35903ea845c491f4e290a7a", size = 41245, upload-time = "2024-10-05T18:43:09.901Z" }, + { url = "https://files.pythonhosted.org/packages/12/46/eba9be9daa403fa94854ce16a458c29df9a01c6c047931c3d8be6016cd9a/pre_commit_hooks-6.0.0-py2.py3-none-any.whl", hash = "sha256:76161b76d321d2f8ee2a8e0b84c30ee8443e01376121fd1c90851e33e3bd7ee2", size = 41338, upload-time = "2025-08-09T19:25:03.513Z" }, ] [[package]] @@ -1566,28 +1562,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, ] -[[package]] -name = "pyarrow" -version = "21.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/dc/80564a3071a57c20b7c32575e4a0120e8a330ef487c319b122942d665960/pyarrow-21.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b", size = 31243234, upload-time = "2025-07-18T00:55:03.812Z" }, - { url = "https://files.pythonhosted.org/packages/ea/cc/3b51cb2db26fe535d14f74cab4c79b191ed9a8cd4cbba45e2379b5ca2746/pyarrow-21.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10", size = 32714370, upload-time = "2025-07-18T00:55:07.495Z" }, - { url = "https://files.pythonhosted.org/packages/24/11/a4431f36d5ad7d83b87146f515c063e4d07ef0b7240876ddb885e6b44f2e/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e", size = 41135424, upload-time = "2025-07-18T00:55:11.461Z" }, - { url = "https://files.pythonhosted.org/packages/74/dc/035d54638fc5d2971cbf1e987ccd45f1091c83bcf747281cf6cc25e72c88/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569", size = 42823810, upload-time = "2025-07-18T00:55:16.301Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3b/89fced102448a9e3e0d4dded1f37fa3ce4700f02cdb8665457fcc8015f5b/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e", size = 43391538, upload-time = "2025-07-18T00:55:23.82Z" }, - { url = "https://files.pythonhosted.org/packages/fb/bb/ea7f1bd08978d39debd3b23611c293f64a642557e8141c80635d501e6d53/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c", size = 45120056, upload-time = "2025-07-18T00:55:28.231Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0b/77ea0600009842b30ceebc3337639a7380cd946061b620ac1a2f3cb541e2/pyarrow-21.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6", size = 26220568, upload-time = "2025-07-18T00:55:32.122Z" }, - { url = "https://files.pythonhosted.org/packages/ca/d4/d4f817b21aacc30195cf6a46ba041dd1be827efa4a623cc8bf39a1c2a0c0/pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd", size = 31160305, upload-time = "2025-07-18T00:55:35.373Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9c/dcd38ce6e4b4d9a19e1d36914cb8e2b1da4e6003dd075474c4cfcdfe0601/pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876", size = 32684264, upload-time = "2025-07-18T00:55:39.303Z" }, - { url = "https://files.pythonhosted.org/packages/4f/74/2a2d9f8d7a59b639523454bec12dba35ae3d0a07d8ab529dc0809f74b23c/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d", size = 41108099, upload-time = "2025-07-18T00:55:42.889Z" }, - { url = "https://files.pythonhosted.org/packages/ad/90/2660332eeb31303c13b653ea566a9918484b6e4d6b9d2d46879a33ab0622/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e", size = 42829529, upload-time = "2025-07-18T00:55:47.069Z" }, - { url = "https://files.pythonhosted.org/packages/33/27/1a93a25c92717f6aa0fca06eb4700860577d016cd3ae51aad0e0488ac899/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82", size = 43367883, upload-time = "2025-07-18T00:55:53.069Z" }, - { url = "https://files.pythonhosted.org/packages/05/d9/4d09d919f35d599bc05c6950095e358c3e15148ead26292dfca1fb659b0c/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623", size = 45133802, upload-time = "2025-07-18T00:55:57.714Z" }, - { url = "https://files.pythonhosted.org/packages/71/30/f3795b6e192c3ab881325ffe172e526499eb3780e306a15103a2764916a2/pyarrow-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18", size = 26203175, upload-time = "2025-07-18T00:56:01.364Z" }, -] - [[package]] name = "pyaudio" version = "0.2.14" @@ -1793,164 +1767,164 @@ name = "pyobjc" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-accessibility", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-accounts", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-addressbook" }, - { name = "pyobjc-framework-adservices", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-adsupport", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-applescriptkit" }, - { name = "pyobjc-framework-applescriptobjc", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-applicationservices" }, - { name = "pyobjc-framework-apptrackingtransparency", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-audiovideobridging", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-authenticationservices", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-automaticassessmentconfiguration", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-automator" }, - { name = "pyobjc-framework-avfoundation", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-avkit", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-avrouting", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-backgroundassets", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-browserenginekit", marker = "platform_release >= '23.4'" }, - { name = "pyobjc-framework-businesschat", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-calendarstore", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-callkit", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-carbon" }, - { name = "pyobjc-framework-cfnetwork" }, - { name = "pyobjc-framework-cinematic", marker = "platform_release >= '23.0'" }, - { name = "pyobjc-framework-classkit", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-cloudkit", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-collaboration", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-colorsync", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-contacts", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-contactsui", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-coreaudio" }, - { name = "pyobjc-framework-coreaudiokit" }, - { name = "pyobjc-framework-corebluetooth", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-coredata" }, - { name = "pyobjc-framework-corehaptics", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-corelocation", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-coremedia", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-coremediaio", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-coremidi" }, - { name = "pyobjc-framework-coreml", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-coremotion", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-coreservices" }, - { name = "pyobjc-framework-corespotlight", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-coretext" }, - { name = "pyobjc-framework-corewlan", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-cryptotokenkit", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-datadetection", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-devicecheck", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-devicediscoveryextension", marker = "platform_release >= '24.0'" }, - { name = "pyobjc-framework-dictionaryservices", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-discrecording" }, - { name = "pyobjc-framework-discrecordingui" }, - { name = "pyobjc-framework-diskarbitration" }, - { name = "pyobjc-framework-dvdplayback" }, - { name = "pyobjc-framework-eventkit", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-exceptionhandling" }, - { name = "pyobjc-framework-executionpolicy", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-extensionkit", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-externalaccessory", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-fileprovider", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-fileproviderui", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-findersync", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-fsevents", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-fskit", marker = "platform_release >= '24.4'" }, - { name = "pyobjc-framework-gamecenter", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-gamecontroller", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-gamekit", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-gameplaykit", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-healthkit", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-imagecapturecore", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-inputmethodkit", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-installerplugins" }, - { name = "pyobjc-framework-instantmessage", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-intents", marker = "platform_release >= '16.0'" }, - { name = "pyobjc-framework-intentsui", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-iobluetooth" }, - { name = "pyobjc-framework-iobluetoothui" }, - { name = "pyobjc-framework-iosurface", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-ituneslibrary", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-kernelmanagement", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-latentsemanticmapping" }, - { name = "pyobjc-framework-launchservices" }, - { name = "pyobjc-framework-libdispatch", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-libxpc", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-linkpresentation", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-localauthentication", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-localauthenticationembeddedui", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-mailkit", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-mapkit", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-mediaaccessibility", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-mediaextension", marker = "platform_release >= '24.0'" }, - { name = "pyobjc-framework-medialibrary", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-mediaplayer", marker = "platform_release >= '16.0'" }, - { name = "pyobjc-framework-mediatoolbox", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-metal", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-metalfx", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-metalkit", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-metalperformanceshaders", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-metalperformanceshadersgraph", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-metrickit", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-mlcompute", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-modelio", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-multipeerconnectivity", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-naturallanguage", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-netfs", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-network", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-networkextension", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-notificationcenter", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-opendirectory", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-osakit" }, - { name = "pyobjc-framework-oslog", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-passkit", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-pencilkit", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-phase", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-photos", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-photosui", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-preferencepanes" }, - { name = "pyobjc-framework-pushkit", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-quartz" }, - { name = "pyobjc-framework-quicklookthumbnailing", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-replaykit", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-safariservices", marker = "platform_release >= '16.0'" }, - { name = "pyobjc-framework-safetykit", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-scenekit", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-screencapturekit", marker = "platform_release >= '21.4'" }, - { name = "pyobjc-framework-screensaver" }, - { name = "pyobjc-framework-screentime", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-scriptingbridge", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-searchkit" }, - { name = "pyobjc-framework-security" }, - { name = "pyobjc-framework-securityfoundation" }, - { name = "pyobjc-framework-securityinterface" }, - { name = "pyobjc-framework-securityui", marker = "platform_release >= '24.4'" }, - { name = "pyobjc-framework-sensitivecontentanalysis", marker = "platform_release >= '23.0'" }, - { name = "pyobjc-framework-servicemanagement", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-sharedwithyou", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-sharedwithyoucore", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-shazamkit", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-social", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-soundanalysis", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-speech", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-spritekit", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-storekit", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-symbols", marker = "platform_release >= '23.0'" }, - { name = "pyobjc-framework-syncservices" }, - { name = "pyobjc-framework-systemconfiguration" }, - { name = "pyobjc-framework-systemextensions", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-threadnetwork", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-uniformtypeidentifiers", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-usernotifications", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-usernotificationsui", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-videosubscriberaccount", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-videotoolbox", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-virtualization", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-vision", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-webkit" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-accessibility", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-accounts", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-addressbook", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-adservices", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-adsupport", marker = "platform_release >= '18.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-applescriptkit", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-applescriptobjc", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-applicationservices", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-apptrackingtransparency", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-audiovideobridging", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-authenticationservices", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-automaticassessmentconfiguration", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-automator", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-avfoundation", marker = "platform_release >= '11.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-avkit", marker = "platform_release >= '13.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-avrouting", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-backgroundassets", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-browserenginekit", marker = "platform_release >= '23.4' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-businesschat", marker = "platform_release >= '18.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-calendarstore", marker = "platform_release >= '9.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-callkit", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-carbon", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cfnetwork", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cinematic", marker = "platform_release >= '23.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-classkit", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cloudkit", marker = "platform_release >= '14.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-collaboration", marker = "platform_release >= '9.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-colorsync", marker = "platform_release >= '17.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-contacts", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-contactsui", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreaudio", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreaudiokit", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corebluetooth", marker = "platform_release >= '14.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coredata", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corehaptics", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corelocation", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "platform_release >= '11.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremediaio", marker = "platform_release >= '11.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremidi", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreml", marker = "platform_release >= '17.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremotion", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreservices", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corespotlight", marker = "platform_release >= '17.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coretext", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corewlan", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cryptotokenkit", marker = "platform_release >= '14.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-datadetection", marker = "platform_release >= '21.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-devicecheck", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-devicediscoveryextension", marker = "platform_release >= '24.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-dictionaryservices", marker = "platform_release >= '9.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-discrecording", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-discrecordingui", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-diskarbitration", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-dvdplayback", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-eventkit", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-exceptionhandling", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-executionpolicy", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-extensionkit", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-externalaccessory", marker = "platform_release >= '17.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-fileprovider", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-fileproviderui", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-findersync", marker = "platform_release >= '14.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-fsevents", marker = "platform_release >= '9.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-fskit", marker = "platform_release >= '24.4' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-gamecenter", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-gamecontroller", marker = "platform_release >= '13.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-gamekit", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-gameplaykit", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-healthkit", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-imagecapturecore", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-inputmethodkit", marker = "platform_release >= '9.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-installerplugins", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-instantmessage", marker = "platform_release >= '9.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-intents", marker = "platform_release >= '16.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-intentsui", marker = "platform_release >= '21.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-iobluetooth", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-iobluetoothui", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-iosurface", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-ituneslibrary", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-kernelmanagement", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-latentsemanticmapping", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-launchservices", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-libdispatch", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-libxpc", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-linkpresentation", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-localauthentication", marker = "platform_release >= '14.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-localauthenticationembeddedui", marker = "platform_release >= '21.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-mailkit", marker = "platform_release >= '21.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-mapkit", marker = "platform_release >= '13.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-mediaaccessibility", marker = "platform_release >= '13.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-mediaextension", marker = "platform_release >= '24.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-medialibrary", marker = "platform_release >= '13.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-mediaplayer", marker = "platform_release >= '16.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-mediatoolbox", marker = "platform_release >= '13.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metal", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metalfx", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metalkit", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metalperformanceshaders", marker = "platform_release >= '17.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metalperformanceshadersgraph", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metrickit", marker = "platform_release >= '21.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-mlcompute", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-modelio", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-multipeerconnectivity", marker = "platform_release >= '14.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-naturallanguage", marker = "platform_release >= '18.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-netfs", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-network", marker = "platform_release >= '18.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-networkextension", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-notificationcenter", marker = "platform_release >= '14.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-opendirectory", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-osakit", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-oslog", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-passkit", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-pencilkit", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-phase", marker = "platform_release >= '21.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-photos", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-photosui", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-preferencepanes", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-pushkit", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quicklookthumbnailing", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-replaykit", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-safariservices", marker = "platform_release >= '16.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-safetykit", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-scenekit", marker = "platform_release >= '11.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-screencapturekit", marker = "platform_release >= '21.4' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-screensaver", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-screentime", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-scriptingbridge", marker = "platform_release >= '9.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-searchkit", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-securityfoundation", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-securityinterface", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-securityui", marker = "platform_release >= '24.4' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-sensitivecontentanalysis", marker = "platform_release >= '23.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-servicemanagement", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-sharedwithyou", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-sharedwithyoucore", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-shazamkit", marker = "platform_release >= '21.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-social", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-soundanalysis", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-speech", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-spritekit", marker = "platform_release >= '13.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-storekit", marker = "platform_release >= '11.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-symbols", marker = "platform_release >= '23.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-syncservices", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-systemconfiguration", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-systemextensions", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-threadnetwork", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-uniformtypeidentifiers", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-usernotifications", marker = "platform_release >= '18.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-usernotificationsui", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-videosubscriberaccount", marker = "platform_release >= '18.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-videotoolbox", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-virtualization", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-vision", marker = "platform_release >= '17.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-webkit", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/5e/16bc372806790d295c76b5c7851767cc9ee3787b3e581f5d7cc44158e4e0/pyobjc-11.1.tar.gz", hash = "sha256:a71b14389657811d658526ba4d5faba4ef7eadbddcf9fe8bf4fb3a6261effba3", size = 11161, upload-time = "2025-06-14T20:56:32.819Z" } wheels = [ @@ -1972,9 +1946,9 @@ name = "pyobjc-framework-accessibility" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/10c16e9d48568a68da2f61866b19468d4ac7129c377d4b1333ee936ae5d0/pyobjc_framework_accessibility-11.1.tar.gz", hash = "sha256:c0fa5f1e00906ec002f582c7d3d80463a46d19f672bf5ec51144f819eeb40656", size = 45098, upload-time = "2025-06-14T20:56:35.287Z" } wheels = [ @@ -1987,8 +1961,8 @@ name = "pyobjc-framework-accounts" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/12/45/ca21003f68ad0f13b5a9ac1761862ad2ddd83224b4314a2f7d03ca437c8d/pyobjc_framework_accounts-11.1.tar.gz", hash = "sha256:384fec156e13ff75253bb094339013f4013464f6dfd47e2f7de3e2ae7441c030", size = 17086, upload-time = "2025-06-14T20:56:36.035Z" } wheels = [ @@ -2000,8 +1974,8 @@ name = "pyobjc-framework-addressbook" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/d3/f5bb5c72be5c6e52224f43e23e5a44e86d2c35ee9af36939e5514c6c7a0f/pyobjc_framework_addressbook-11.1.tar.gz", hash = "sha256:ce2db3be4a3128bf79d5c41319a6d16b73754785ce75ac694d0d658c690922fc", size = 97609, upload-time = "2025-06-14T20:56:37.324Z" } wheels = [ @@ -2014,8 +1988,8 @@ name = "pyobjc-framework-adservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2a/3f/af76eab6eee0a405a4fdee172e7181773040158476966ecd757b0a98bfc5/pyobjc_framework_adservices-11.1.tar.gz", hash = "sha256:44c72f8163705c9aa41baca938fdb17dde257639e5797e6a5c3a2b2d8afdade9", size = 12473, upload-time = "2025-06-14T20:56:38.147Z" } wheels = [ @@ -2027,8 +2001,8 @@ name = "pyobjc-framework-adsupport" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7f/03/9c51edd964796a97def4e1433d76a128dd7059b685fb4366081bf4e292ba/pyobjc_framework_adsupport-11.1.tar.gz", hash = "sha256:78b9667c275785df96219d205bd4309731869c3298d0931e32aed83bede29096", size = 12556, upload-time = "2025-06-14T20:56:38.741Z" } wheels = [ @@ -2040,8 +2014,8 @@ name = "pyobjc-framework-applescriptkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/63/1bcfcdca53bf5bba3a7b4d73d24232ae1721a378a32fd4ebc34a35549df2/pyobjc_framework_applescriptkit-11.1.tar.gz", hash = "sha256:477707352eaa6cc4a5f8c593759dc3227a19d5958481b1482f0d59394a4601c3", size = 12392, upload-time = "2025-06-14T20:56:39.331Z" } wheels = [ @@ -2053,8 +2027,8 @@ name = "pyobjc-framework-applescriptobjc" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/27/687b55b575367df045879b786f358355e40e41f847968e557d0718a6c4a4/pyobjc_framework_applescriptobjc-11.1.tar.gz", hash = "sha256:c8a0ec975b64411a4f16a1280c5ea8dbe949fd361e723edd343102f0f95aba6e", size = 12445, upload-time = "2025-06-14T20:56:39.976Z" } wheels = [ @@ -2066,10 +2040,10 @@ name = "pyobjc-framework-applicationservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coretext" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coretext", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/3f/b33ce0cecc3a42f6c289dcbf9ff698b0d9e85f5796db2e9cb5dadccffbb9/pyobjc_framework_applicationservices-11.1.tar.gz", hash = "sha256:03fcd8c0c600db98fa8b85eb7b3bc31491701720c795e3f762b54e865138bbaf", size = 224842, upload-time = "2025-06-14T20:56:40.648Z" } wheels = [ @@ -2082,8 +2056,8 @@ name = "pyobjc-framework-apptrackingtransparency" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/68/7aa3afffd038dd6e5af764336bca734eb910121013ca71030457b61e5b99/pyobjc_framework_apptrackingtransparency-11.1.tar.gz", hash = "sha256:796cc5f83346c10973806cfb535d4200b894a5d2626ff2eeb1972d594d14fed4", size = 13135, upload-time = "2025-06-14T20:56:41.494Z" } wheels = [ @@ -2095,8 +2069,8 @@ name = "pyobjc-framework-audiovideobridging" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c3/25/6c5a7b1443d30139cc722029880284ea9dfa575f0436471b9364fcd499f5/pyobjc_framework_audiovideobridging-11.1.tar.gz", hash = "sha256:12756b3aa35083b8ad5c9139b6a0e2f4792e217096b5bf6b702d499038203991", size = 72913, upload-time = "2025-06-14T20:56:42.128Z" } wheels = [ @@ -2109,8 +2083,8 @@ name = "pyobjc-framework-authenticationservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8f/b7/3e9ad0ed3625dc02e495615ea5dbf55ca95cbd25b3e31f25092f5caad640/pyobjc_framework_authenticationservices-11.1.tar.gz", hash = "sha256:8fd801cdb53d426b4e678b0a8529c005d0c44f5a17ccd7052a7c3a1a87caed6a", size = 115266, upload-time = "2025-06-14T20:56:42.889Z" } wheels = [ @@ -2123,8 +2097,8 @@ name = "pyobjc-framework-automaticassessmentconfiguration" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/39/d4c94e0245d290b83919854c4f205851cc0b2603f843448fdfb8e74aad71/pyobjc_framework_automaticassessmentconfiguration-11.1.tar.gz", hash = "sha256:70eadbf8600101901a56fcd7014d8941604e14f3b3728bc4fb0178a9a9420032", size = 24933, upload-time = "2025-06-14T20:56:43.984Z" } wheels = [ @@ -2137,8 +2111,8 @@ name = "pyobjc-framework-automator" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/9f/097ed9f4de9e9491a1b08bb7d85d35a95d726c9e9f5f5bf203b359a436b6/pyobjc_framework_automator-11.1.tar.gz", hash = "sha256:9b46c55a4f9ae2b3c39ff560f42ced66bdd18c093188f0b5fc4060ad911838e4", size = 201439, upload-time = "2025-06-14T20:56:44.767Z" } wheels = [ @@ -2151,11 +2125,11 @@ name = "pyobjc-framework-avfoundation" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coreaudio" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreaudio", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3c/1f/90cdbce1d3b4861cbb17c12adf57daeec32477eb1df8d3f9ab8551bdadfb/pyobjc_framework_avfoundation-11.1.tar.gz", hash = "sha256:6663056cc6ca49af8de6d36a7fff498f51e1a9a7f1bde7afba718a8ceaaa7377", size = 832178, upload-time = "2025-06-14T20:56:46.329Z" } wheels = [ @@ -2168,9 +2142,9 @@ name = "pyobjc-framework-avkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/ff/9f41f2b8de786871184b48c4e5052cb7c9fcc204e7fee06687fa32b08bed/pyobjc_framework_avkit-11.1.tar.gz", hash = "sha256:d948204a7b94e0e878b19a909f9b33342e19d9ea519571d66a21fce8f72e3263", size = 46825, upload-time = "2025-06-14T20:56:47.494Z" } wheels = [ @@ -2183,8 +2157,8 @@ name = "pyobjc-framework-avrouting" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cf/42/94bc18b968a4ee8b6427257f907ffbfc97f8ba6a6202953da149b649d638/pyobjc_framework_avrouting-11.1.tar.gz", hash = "sha256:7db1291d9f53cc58d34b2a826feb721a85f50ceb5e71952e8762baacd3db3fc0", size = 21069, upload-time = "2025-06-14T20:56:48.57Z" } wheels = [ @@ -2197,8 +2171,8 @@ name = "pyobjc-framework-backgroundassets" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/76/21e1632a212f997d7a5f26d53eb997951978916858039b79f43ebe3d10b2/pyobjc_framework_backgroundassets-11.1.tar.gz", hash = "sha256:2e14b50539d96d5fca70c49f21b69fdbad81a22549e3630f5e4f20d5c0204fc2", size = 24803, upload-time = "2025-06-14T20:56:49.566Z" } wheels = [ @@ -2211,11 +2185,11 @@ name = "pyobjc-framework-browserenginekit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coreaudio" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreaudio", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/30/75/087270d9f81e913b57c7db58eaff8691fa0574b11faf9302340b3b8320f1/pyobjc_framework_browserenginekit-11.1.tar.gz", hash = "sha256:918440cefb10480024f645169de3733e30ede65e41267fa12c7b90c264a0a479", size = 31944, upload-time = "2025-06-14T20:56:50.195Z" } wheels = [ @@ -2228,8 +2202,8 @@ name = "pyobjc-framework-businesschat" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/85/be/9d9d9d9383c411a58323ea510d768443287ca21610af652b815b3205ea80/pyobjc_framework_businesschat-11.1.tar.gz", hash = "sha256:69589d2f0cb4e7892e5ecc6aed79b1abd1ec55c099a7faacae6a326bc921259d", size = 12698, upload-time = "2025-06-14T20:56:51.173Z" } wheels = [ @@ -2241,8 +2215,8 @@ name = "pyobjc-framework-calendarstore" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/df/7ca8ee65b16d5fc862d7e8664289472eed918cf4d76921de6bdaa1461c65/pyobjc_framework_calendarstore-11.1.tar.gz", hash = "sha256:858ee00e6a380d9c086c2d7db82c116a6c406234038e0ec8fc2ad02e385dc437", size = 68215, upload-time = "2025-06-14T20:56:51.799Z" } wheels = [ @@ -2254,8 +2228,8 @@ name = "pyobjc-framework-callkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/51/d5/4f0b62ab35be619e8c8d96538a03cf56fde6fd53540e1837e0fa588b3f6c/pyobjc_framework_callkit-11.1.tar.gz", hash = "sha256:b84d5ea38dff0cbe0754f5f9f6f33c742e216f12e7166179a8ec2cf4b0bfca94", size = 46648, upload-time = "2025-06-14T20:56:52.579Z" } wheels = [ @@ -2268,8 +2242,8 @@ name = "pyobjc-framework-carbon" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/39/a4/d751851865d9a78405cfec0c8b2931b1e96b9914e9788cd441fa4e8290d0/pyobjc_framework_carbon-11.1.tar.gz", hash = "sha256:047f098535479efa3ab89da1ebdf3cf9ec0b439a33a4f32806193886e9fcea71", size = 37291, upload-time = "2025-06-14T20:56:53.642Z" } wheels = [ @@ -2281,8 +2255,8 @@ name = "pyobjc-framework-cfnetwork" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/49/7b24172e3d6eb0ddffc33a7498a2bea264aa2958c3fecaeb463bef88f0b8/pyobjc_framework_cfnetwork-11.1.tar.gz", hash = "sha256:ad600163eeadb7bf71abc51a9b6f2b5462a018d3f9bb1510c5ce3fdf2f22959d", size = 79069, upload-time = "2025-06-14T20:56:54.615Z" } wheels = [ @@ -2295,11 +2269,11 @@ name = "pyobjc-framework-cinematic" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-avfoundation" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-metal" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-avfoundation", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metal", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/57/6f/c2d0b49e01e654496a1781bafb9da72a6fbd00f5abb39dc4a3a0045167c7/pyobjc_framework_cinematic-11.1.tar.gz", hash = "sha256:efde39a6a2379e1738dbc5434b2470cd187cf3114ffb81390b3b1abda470b382", size = 25522, upload-time = "2025-06-14T20:56:55.379Z" } wheels = [ @@ -2311,8 +2285,8 @@ name = "pyobjc-framework-classkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/8b/5150b4faddd15d5dd795bc62b2256c4f7dafc983cfa694fcf88121ea0016/pyobjc_framework_classkit-11.1.tar.gz", hash = "sha256:ee1e26395eb00b3ed5442e3234cdbfe925d2413185af38eca0477d7166651df4", size = 39831, upload-time = "2025-06-14T20:56:56.036Z" } wheels = [ @@ -2325,11 +2299,11 @@ name = "pyobjc-framework-cloudkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-accounts" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coredata" }, - { name = "pyobjc-framework-corelocation" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-accounts", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coredata", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corelocation", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/a6/bfe5be55ed95704efca0e86b218155a9c801735107cedba3af8ea4580a05/pyobjc_framework_cloudkit-11.1.tar.gz", hash = "sha256:40d2dc4bf28c5be9b836b01e4d267a15d847d756c2a65530e1fcd79b2825e86d", size = 122778, upload-time = "2025-06-14T20:56:56.73Z" } wheels = [ @@ -2341,7 +2315,7 @@ name = "pyobjc-framework-cocoa" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4b/c5/7a866d24bc026f79239b74d05e2cf3088b03263da66d53d1b4cf5207f5ae/pyobjc_framework_cocoa-11.1.tar.gz", hash = "sha256:87df76b9b73e7ca699a828ff112564b59251bb9bbe72e610e670a4dc9940d038", size = 5565335, upload-time = "2025-06-14T20:56:59.683Z" } wheels = [ @@ -2354,8 +2328,8 @@ name = "pyobjc-framework-collaboration" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/49/9dbe8407d5dd663747267c1234d1b914bab66e1878d22f57926261a3063b/pyobjc_framework_collaboration-11.1.tar.gz", hash = "sha256:4564e3931bfc51773623d4f57f2431b58a39b75cb964ae5c48d27ee4dde2f4ea", size = 16839, upload-time = "2025-06-14T20:57:01.101Z" } wheels = [ @@ -2367,8 +2341,8 @@ name = "pyobjc-framework-colorsync" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b5/97/7613b6041f62c52f972e42dd5d79476b56b84d017a8b5e4add4d9cfaca36/pyobjc_framework_colorsync-11.1.tar.gz", hash = "sha256:7a346f71f34b2ccd1b020a34c219b85bf8b6f6e05283d503185aeb7767a269dd", size = 38999, upload-time = "2025-06-14T20:57:01.761Z" } wheels = [ @@ -2380,8 +2354,8 @@ name = "pyobjc-framework-contacts" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/85/34868b6447d552adf8674bac226b55c2baacacee0d67ee031e33805d6faa/pyobjc_framework_contacts-11.1.tar.gz", hash = "sha256:752036e7d8952a4122296d7772f274170a5f35a53ee6454a27f3e1d9603222cc", size = 84814, upload-time = "2025-06-14T20:57:02.582Z" } wheels = [ @@ -2394,9 +2368,9 @@ name = "pyobjc-framework-contactsui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-contacts" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-contacts", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3f/57/8765b54a30edaa2a56df62e11e7c32e41b6ea300513256adffa191689368/pyobjc_framework_contactsui-11.1.tar.gz", hash = "sha256:5bc29ea2b10a342018e1b96be6b140c10ebe3cfb6417278770feef5e88026a1f", size = 20031, upload-time = "2025-06-14T20:57:03.603Z" } wheels = [ @@ -2409,8 +2383,8 @@ name = "pyobjc-framework-coreaudio" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/39/c0/4ab6005cf97e534725b0c14b110d4864b367c282b1c5b0d8f42aad74a83f/pyobjc_framework_coreaudio-11.1.tar.gz", hash = "sha256:b7b89540ae7efc6c1e3208ac838ef2acfc4d2c506dd629d91f6b3b3120e55c1b", size = 141032, upload-time = "2025-06-14T20:57:04.348Z" } wheels = [ @@ -2423,9 +2397,9 @@ name = "pyobjc-framework-coreaudiokit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coreaudio" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreaudio", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/4e/c49b26c60047c511727efe994b412276c487dfe90f1ee0fced0bddbdf8a3/pyobjc_framework_coreaudiokit-11.1.tar.gz", hash = "sha256:0b461c3d6123fda4da6b6aaa022efc918c1de2e126a5cf07d2189d63fa54ba40", size = 21955, upload-time = "2025-06-14T20:57:05.218Z" } wheels = [ @@ -2438,8 +2412,8 @@ name = "pyobjc-framework-corebluetooth" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/fe/2081dfd9413b7b4d719935c33762fbed9cce9dc06430f322d1e2c9dbcd91/pyobjc_framework_corebluetooth-11.1.tar.gz", hash = "sha256:1deba46e3fcaf5e1c314f4bbafb77d9fe49ec248c493ad00d8aff2df212d6190", size = 60337, upload-time = "2025-06-14T20:57:05.919Z" } wheels = [ @@ -2452,8 +2426,8 @@ name = "pyobjc-framework-coredata" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/e3/af497da7a7c895b6ff529d709d855a783f34afcc4b87ab57a1a2afb3f876/pyobjc_framework_coredata-11.1.tar.gz", hash = "sha256:fe9fd985f8e06c70c0fb1e6bbea5b731461f9e76f8f8d8e89c7c72667cdc6adf", size = 260628, upload-time = "2025-06-14T20:57:06.729Z" } wheels = [ @@ -2466,8 +2440,8 @@ name = "pyobjc-framework-corehaptics" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5f/83/cc997ec4687a68214dd3ad1bdf64353305f5c7e827fad211adac4c28b39f/pyobjc_framework_corehaptics-11.1.tar.gz", hash = "sha256:e5da3a97ed6aca9b7268c8c5196c0a339773a50baa72d1502d3435dc1a2a80f1", size = 42722, upload-time = "2025-06-14T20:57:08.019Z" } wheels = [ @@ -2479,8 +2453,8 @@ name = "pyobjc-framework-corelocation" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/ef/fbd2e01ec137208af7bfefe222773748d27f16f845b0efa950d65e2bd719/pyobjc_framework_corelocation-11.1.tar.gz", hash = "sha256:46a67b99925ee3d53914331759c6ee110b31bb790b74b05915acfca41074c206", size = 104508, upload-time = "2025-06-14T20:57:08.731Z" } wheels = [ @@ -2493,8 +2467,8 @@ name = "pyobjc-framework-coremedia" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/5d/81513acd219df77a89176f1574d936b81ad6f6002225cabb64d55efb7e8d/pyobjc_framework_coremedia-11.1.tar.gz", hash = "sha256:82cdc087f61e21b761e677ea618a575d4c0dbe00e98230bf9cea540cff931db3", size = 216389, upload-time = "2025-06-14T20:57:09.546Z" } wheels = [ @@ -2507,8 +2481,8 @@ name = "pyobjc-framework-coremediaio" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/64/68/9cef2aefba8e69916049ff43120e8794df8051bdf1f690a55994bbe4eb57/pyobjc_framework_coremediaio-11.1.tar.gz", hash = "sha256:bccd69712578b177144ded398f4695d71a765ef61204da51a21f0c90b4ad4c64", size = 108326, upload-time = "2025-06-14T20:57:10.435Z" } wheels = [ @@ -2521,8 +2495,8 @@ name = "pyobjc-framework-coremidi" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ca/2ae5149966ccd78290444f88fa62022e2b96ed2fddd47e71d9fd249a9f82/pyobjc_framework_coremidi-11.1.tar.gz", hash = "sha256:095030c59d50c23aa53608777102bc88744ff8b10dfb57afe24b428dcd12e376", size = 107817, upload-time = "2025-06-14T20:57:11.245Z" } wheels = [ @@ -2535,8 +2509,8 @@ name = "pyobjc-framework-coreml" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0d/5d/4309f220981d769b1a2f0dcb2c5c104490d31389a8ebea67e5595ce1cb74/pyobjc_framework_coreml-11.1.tar.gz", hash = "sha256:775923eefb9eac2e389c0821b10564372de8057cea89f1ea1cdaf04996c970a7", size = 82005, upload-time = "2025-06-14T20:57:12.004Z" } wheels = [ @@ -2549,8 +2523,8 @@ name = "pyobjc-framework-coremotion" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a5/95/e469dc7100ea6b9c29a074a4f713d78b32a78d7ec5498c25c83a56744fc2/pyobjc_framework_coremotion-11.1.tar.gz", hash = "sha256:5884a568521c0836fac39d46683a4dea3d259a23837920897042ffb922d9ac3e", size = 67050, upload-time = "2025-06-14T20:57:12.705Z" } wheels = [ @@ -2563,9 +2537,9 @@ name = "pyobjc-framework-coreservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-fsevents" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-fsevents", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/a9/141d18019a25776f507992f9e7ffc051ca5a734848d8ea8d848f7c938efc/pyobjc_framework_coreservices-11.1.tar.gz", hash = "sha256:cf8eb5e272c60a96d025313eca26ff2487dcd02c47034cc9db39f6852d077873", size = 1245086, upload-time = "2025-06-14T20:57:13.914Z" } wheels = [ @@ -2578,8 +2552,8 @@ name = "pyobjc-framework-corespotlight" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/31/c7/b67ebfb63b7ccbfda780d583056d1fd4b610ba3839c8ebe3435b86122c61/pyobjc_framework_corespotlight-11.1.tar.gz", hash = "sha256:4dd363c8d3ff7619659b63dd31400f135b03e32435b5d151459ecdacea14e0f2", size = 87161, upload-time = "2025-06-14T20:57:14.934Z" } wheels = [ @@ -2592,9 +2566,9 @@ name = "pyobjc-framework-coretext" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/65/e9/d3231c4f87d07b8525401fd6ad3c56607c9e512c5490f0a7a6abb13acab6/pyobjc_framework_coretext-11.1.tar.gz", hash = "sha256:a29bbd5d85c77f46a8ee81d381b847244c88a3a5a96ac22f509027ceceaffaf6", size = 274702, upload-time = "2025-06-14T20:57:16.059Z" } wheels = [ @@ -2607,8 +2581,8 @@ name = "pyobjc-framework-corewlan" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c6/d8/03aff3c75485fc999e260946ef1e9adf17640a6e08d7bf603d31cfcf73fc/pyobjc_framework_corewlan-11.1.tar.gz", hash = "sha256:4a8afea75393cc0a6fe696e136233aa0ed54266f35a47b55a3583f4cb078e6ce", size = 65792, upload-time = "2025-06-14T20:57:16.931Z" } wheels = [ @@ -2621,8 +2595,8 @@ name = "pyobjc-framework-cryptotokenkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/92/7fab6fcc6bb659d6946cfb2f670058180bcc4ca1626878b0f7c95107abf0/pyobjc_framework_cryptotokenkit-11.1.tar.gz", hash = "sha256:5f82f44d9ab466c715a7c8ad4d5ec47c68aacd78bd67b5466a7b8215a2265328", size = 59223, upload-time = "2025-06-14T20:57:17.658Z" } wheels = [ @@ -2635,8 +2609,8 @@ name = "pyobjc-framework-datadetection" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/4d/65c61d8878b44689e28d5729be9edbb73e20b1b0500d1095172cfd24aea6/pyobjc_framework_datadetection-11.1.tar.gz", hash = "sha256:cbe0080b51e09b2f91eaf2a9babec3dcf2883d7966bc0abd8393ef7abfcfc5db", size = 13485, upload-time = "2025-06-14T20:57:18.829Z" } wheels = [ @@ -2648,8 +2622,8 @@ name = "pyobjc-framework-devicecheck" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/f2/b1d263f8231f815a9eeff15809f4b7428dacdc0a6aa267db5ed907445066/pyobjc_framework_devicecheck-11.1.tar.gz", hash = "sha256:8b05973eb2673571144d81346336e749a21cec90bd7fcaade76ffd3b147a0741", size = 13954, upload-time = "2025-06-14T20:57:19.782Z" } wheels = [ @@ -2661,8 +2635,8 @@ name = "pyobjc-framework-devicediscoveryextension" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/b8/102863bfa2f1e414c88bb9f51151a9a58b99c268a841b59d46e0dcc5fe6d/pyobjc_framework_devicediscoveryextension-11.1.tar.gz", hash = "sha256:ae160ea40f25d3ee5e7ce80ac9c1b315f94d0a4c7ccb86920396f71c6bf799a0", size = 14298, upload-time = "2025-06-14T20:57:20.738Z" } wheels = [ @@ -2674,8 +2648,8 @@ name = "pyobjc-framework-dictionaryservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-coreservices" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreservices", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d6/13/c46f6db61133fee15e3471f33a679da2af10d63fa2b4369e0cd476988721/pyobjc_framework_dictionaryservices-11.1.tar.gz", hash = "sha256:39c24452d0ddd037afeb73a1742614c94535f15b1c024a8a6cc7ff081e1d22e7", size = 10578, upload-time = "2025-06-14T20:57:21.392Z" } wheels = [ @@ -2687,8 +2661,8 @@ name = "pyobjc-framework-discrecording" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a5/b2/d8d1a28643c2ab681b517647bacb68496c98886336ffbd274f0b2ad28cdc/pyobjc_framework_discrecording-11.1.tar.gz", hash = "sha256:37585458e363b20bb28acdb5cc265dfca934d8a07b7baed2584953c11c927a87", size = 123004, upload-time = "2025-06-14T20:57:22.01Z" } wheels = [ @@ -2701,9 +2675,9 @@ name = "pyobjc-framework-discrecordingui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-discrecording" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-discrecording", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/53/d71717f00332b8fc3d8a5c7234fdc270adadfeb5ca9318a55986f5c29c44/pyobjc_framework_discrecordingui-11.1.tar.gz", hash = "sha256:a9f10e2e7ee19582c77f0755ae11a64e3d61c652cbd8a5bf52756f599be24797", size = 19370, upload-time = "2025-06-14T20:57:22.791Z" } wheels = [ @@ -2715,8 +2689,8 @@ name = "pyobjc-framework-diskarbitration" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/2a/68fa0c99e04ec1ec24b0b7d6f5b7ec735d5e8a73277c5c0671438a69a403/pyobjc_framework_diskarbitration-11.1.tar.gz", hash = "sha256:a933efc6624779a393fafe0313e43378bcae2b85d6d15cff95ac30048c1ef490", size = 19866, upload-time = "2025-06-14T20:57:23.435Z" } wheels = [ @@ -2728,8 +2702,8 @@ name = "pyobjc-framework-dvdplayback" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b8/76/77046325b1957f0cbcdf4f96667496d042ed4758f3413f1d21df5b085939/pyobjc_framework_dvdplayback-11.1.tar.gz", hash = "sha256:b44c36a62c8479e649133216e22941859407cca5796b5f778815ef9340a838f4", size = 64558, upload-time = "2025-06-14T20:57:24.118Z" } wheels = [ @@ -2741,8 +2715,8 @@ name = "pyobjc-framework-eventkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/c4/cbba8f2dce13b9be37ecfd423ba2b92aa3f209dbb58ede6c4ce3b242feee/pyobjc_framework_eventkit-11.1.tar.gz", hash = "sha256:5643150f584243681099c5e9435efa833a913e93fe9ca81f62007e287349b561", size = 75177, upload-time = "2025-06-14T20:57:24.81Z" } wheels = [ @@ -2754,8 +2728,8 @@ name = "pyobjc-framework-exceptionhandling" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/0d/c72a885b40d28a99b586447f9ea6f400589f13d554fcd6f13a2c841bb6d2/pyobjc_framework_exceptionhandling-11.1.tar.gz", hash = "sha256:e010f56bf60ab4e9e3225954ebb53e9d7135d37097043ac6dd2a3f35770d4efa", size = 17890, upload-time = "2025-06-14T20:57:25.521Z" } wheels = [ @@ -2767,8 +2741,8 @@ name = "pyobjc-framework-executionpolicy" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/cf/54431846508c5d5bb114a415ebb96187da5847105918169e42f4ca3b00e6/pyobjc_framework_executionpolicy-11.1.tar.gz", hash = "sha256:3280ad2f4c5eaf45901f310cee0c52db940c0c63e959ad082efb8df41055d986", size = 13496, upload-time = "2025-06-14T20:57:26.173Z" } wheels = [ @@ -2780,8 +2754,8 @@ name = "pyobjc-framework-extensionkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ce/7d/89adf16c7de4246477714dce8fcffae4242778aecd0c5f0ad9904725f42c/pyobjc_framework_extensionkit-11.1.tar.gz", hash = "sha256:c114a96f13f586dbbab8b6219a92fa4829896a645c8cd15652a6215bc8ff5409", size = 19766, upload-time = "2025-06-14T20:57:27.106Z" } wheels = [ @@ -2794,8 +2768,8 @@ name = "pyobjc-framework-externalaccessory" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/a3/519242e6822e1ddc9e64e21f717529079dbc28a353474420da8315d0a8b1/pyobjc_framework_externalaccessory-11.1.tar.gz", hash = "sha256:50887e948b78a1d94646422c243ac2a9e40761675e38b9184487870a31e83371", size = 23123, upload-time = "2025-06-14T20:57:27.845Z" } wheels = [ @@ -2808,8 +2782,8 @@ name = "pyobjc-framework-fileprovider" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1b/80/3ebba2c1e5e3aeae989fe038c259a93e7e7e18fd56666ece514d000d38ea/pyobjc_framework_fileprovider-11.1.tar.gz", hash = "sha256:748ca1c75f84afdf5419346a24bf8eec44dca071986f31f00071dc191b3e9ca8", size = 91696, upload-time = "2025-06-14T20:57:28.546Z" } wheels = [ @@ -2822,8 +2796,8 @@ name = "pyobjc-framework-fileproviderui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-fileprovider" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-fileprovider", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/75/ed/0f5af06869661822c4a70aacd674da5d1e6b6661240e2883bbc7142aa525/pyobjc_framework_fileproviderui-11.1.tar.gz", hash = "sha256:162a23e67f59e1bb247e84dda88d513d7944d815144901a46be6fe051b6c7970", size = 13163, upload-time = "2025-06-14T20:57:29.568Z" } wheels = [ @@ -2835,8 +2809,8 @@ name = "pyobjc-framework-findersync" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2a/82/c6b670494ac0c4cf14cf2db0dfbe0df71925d20595404939383ddbcc56d3/pyobjc_framework_findersync-11.1.tar.gz", hash = "sha256:692364937f418f0e4e4abd395a09a7d4a0cdd55fd4e0184de85ee59642defb6e", size = 15045, upload-time = "2025-06-14T20:57:30.173Z" } wheels = [ @@ -2848,8 +2822,8 @@ name = "pyobjc-framework-fsevents" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8e/83/ec0b9ba355dbc34f27ed748df9df4eb6dbfdd9bbd614b0f193752f36f419/pyobjc_framework_fsevents-11.1.tar.gz", hash = "sha256:d29157d04124503c4dfa9dcbbdc8c34d3bab134d3db3a48d96d93f26bd94c14d", size = 29587, upload-time = "2025-06-14T20:57:30.796Z" } wheels = [ @@ -2862,8 +2836,8 @@ name = "pyobjc-framework-fskit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/47/d1f04c6115fa78936399a389cc5e0e443f8341c9a6c1c0df7f6fdbe51286/pyobjc_framework_fskit-11.1.tar.gz", hash = "sha256:9ded1eab19b4183cb04381e554bbbe679c1213fd58599d6fc6e135e93b51136f", size = 42091, upload-time = "2025-06-14T20:57:31.504Z" } wheels = [ @@ -2876,8 +2850,8 @@ name = "pyobjc-framework-gamecenter" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1b/8e/b594fd1dc32a59462fc68ad502be2bd87c70e6359b4e879a99bcc4beaf5b/pyobjc_framework_gamecenter-11.1.tar.gz", hash = "sha256:a1c4ed54e11a6e4efba6f2a21ace92bcf186e3fe5c74a385b31f6b1a515ec20c", size = 31981, upload-time = "2025-06-14T20:57:32.192Z" } wheels = [ @@ -2890,8 +2864,8 @@ name = "pyobjc-framework-gamecontroller" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/70/4c/1dd62103092a182f2ab8904c8a8e3922d2b0a80a7adab0c20e5fd0207d75/pyobjc_framework_gamecontroller-11.1.tar.gz", hash = "sha256:4d5346faf90e1ebe5602c0c480afbf528a35a7a1ad05f9b49991fdd2a97f105b", size = 115783, upload-time = "2025-06-14T20:57:32.879Z" } wheels = [ @@ -2904,9 +2878,9 @@ name = "pyobjc-framework-gamekit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/7b/ba141ec0f85ca816f493d1f6fe68c72d01092e5562e53c470a0111d9c34b/pyobjc_framework_gamekit-11.1.tar.gz", hash = "sha256:9b8db075da8866c4ef039a165af227bc29393dc11a617a40671bf6b3975ae269", size = 165397, upload-time = "2025-06-14T20:57:33.711Z" } wheels = [ @@ -2919,9 +2893,9 @@ name = "pyobjc-framework-gameplaykit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-spritekit" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-spritekit", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/07/f38b1d83eac10ea4f75c605ffc4850585740db89b90842d311e586ee36cd/pyobjc_framework_gameplaykit-11.1.tar.gz", hash = "sha256:9ae2bee69b0cc1afa0e210b4663c7cdbb3cc94be1374808df06f98f992e83639", size = 73399, upload-time = "2025-06-14T20:57:34.538Z" } wheels = [ @@ -2934,8 +2908,8 @@ name = "pyobjc-framework-healthkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/af/66/fa76f7c8e36e4c10677d42d91a8e220c135c610a06b759571db1abe26a32/pyobjc_framework_healthkit-11.1.tar.gz", hash = "sha256:20f59bd9e1ffafe5893b4eff5867fdfd20bd46c3d03bc4009219d82fc6815f76", size = 202009, upload-time = "2025-06-14T20:57:35.285Z" } wheels = [ @@ -2948,8 +2922,8 @@ name = "pyobjc-framework-imagecapturecore" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7b/3b/f4edbc58a8c7394393f8d00d0e764f655545e743ee4e33917f27b8c68e7b/pyobjc_framework_imagecapturecore-11.1.tar.gz", hash = "sha256:a610ceb6726e385b132a1481a68ce85ccf56f94667b6d6e1c45a2cfab806a624", size = 100398, upload-time = "2025-06-14T20:57:36.503Z" } wheels = [ @@ -2962,8 +2936,8 @@ name = "pyobjc-framework-inputmethodkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/32/6a90bba682a31960ba1fc2d3b263e9be26043c4fb7aed273c13647c8b7d9/pyobjc_framework_inputmethodkit-11.1.tar.gz", hash = "sha256:7037579524041dcee71a649293c2660f9359800455a15e6a2f74a17b46d78496", size = 27203, upload-time = "2025-06-14T20:57:37.246Z" } wheels = [ @@ -2976,8 +2950,8 @@ name = "pyobjc-framework-installerplugins" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/89/9a881e466476ca21f3ff3e8e87ccfba1aaad9b88f7eea4be6d3f05b07107/pyobjc_framework_installerplugins-11.1.tar.gz", hash = "sha256:363e59c7e05553d881f0facd41884f17b489ff443d7856e33dd0312064c746d9", size = 27451, upload-time = "2025-06-14T20:57:37.915Z" } wheels = [ @@ -2989,9 +2963,9 @@ name = "pyobjc-framework-instantmessage" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/b9/5cec4dd0053b5f63c01211a60a286c47464d9f3e0c81bd682e6542dbff00/pyobjc_framework_instantmessage-11.1.tar.gz", hash = "sha256:c222aa61eb009704b333f6e63df01a0e690136e7e495907e5396882779bf9525", size = 33774, upload-time = "2025-06-14T20:57:38.553Z" } wheels = [ @@ -3003,8 +2977,8 @@ name = "pyobjc-framework-intents" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4c/af/d7f260d06b79acca8028e373c2fe30bf0be014388ba612f538f40597d929/pyobjc_framework_intents-11.1.tar.gz", hash = "sha256:13185f206493f45d6bd2d4903c2136b1c4f8b9aa37628309ace6ff4a906b4695", size = 448459, upload-time = "2025-06-14T20:57:39.589Z" } wheels = [ @@ -3017,8 +2991,8 @@ name = "pyobjc-framework-intentsui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-intents" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-intents", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/46/20aae4a71efb514b096f36273a6129b48b01535bf501e5719d4a97fcb3a5/pyobjc_framework_intentsui-11.1.tar.gz", hash = "sha256:c8182155af4dce369c18d6e6ed9c25bbd8110c161ed5f1b4fb77cf5cdb99d135", size = 21305, upload-time = "2025-06-14T20:57:40.477Z" } wheels = [ @@ -3031,8 +3005,8 @@ name = "pyobjc-framework-iobluetooth" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/e0/74b7b10c567b66c5f38b45ab240336325a4c889f43072d90f2b90aaeb7c0/pyobjc_framework_iobluetooth-11.1.tar.gz", hash = "sha256:094fd4be60cd1371b17cb4b33a3894e0d88a11b36683912be0540a7d51de76f1", size = 300992, upload-time = "2025-06-14T20:57:41.256Z" } wheels = [ @@ -3045,8 +3019,8 @@ name = "pyobjc-framework-iobluetoothui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-iobluetooth" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-iobluetooth", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dd/32/872272faeab6fe471eac6962c75db72ce65c3556e00b4edebdb41aaab7cb/pyobjc_framework_iobluetoothui-11.1.tar.gz", hash = "sha256:060c721f1cd8af4452493e8153b72b572edcd2a7e3b635d79d844f885afee860", size = 22835, upload-time = "2025-06-14T20:57:42.119Z" } wheels = [ @@ -3058,8 +3032,8 @@ name = "pyobjc-framework-iosurface" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/ce/38ec17d860d0ee040bb737aad8ca7c7ff46bef6c9cffa47382d67682bb2d/pyobjc_framework_iosurface-11.1.tar.gz", hash = "sha256:a468b3a31e8cd70a2675a3ddc7176ab13aa521c035f11188b7a3af8fff8b148b", size = 20275, upload-time = "2025-06-14T20:57:42.742Z" } wheels = [ @@ -3071,8 +3045,8 @@ name = "pyobjc-framework-ituneslibrary" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ee/43/aebefed774b434965752f9001685af0b19c02353aa7a12d2918af0948181/pyobjc_framework_ituneslibrary-11.1.tar.gz", hash = "sha256:e2212a9340e4328056ade3c2f9d4305c71f3f6af050204a135f9fa9aa3ba9c5e", size = 47388, upload-time = "2025-06-14T20:57:43.383Z" } wheels = [ @@ -3084,8 +3058,8 @@ name = "pyobjc-framework-kernelmanagement" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/b6/708f10ac16425834cb5f8b71efdbe39b42c3b1009ac0c1796a42fc98cd36/pyobjc_framework_kernelmanagement-11.1.tar.gz", hash = "sha256:e934d1638cd89e38d6c6c5d4d9901b4295acee2d39cbfe0bd91aae9832961b44", size = 12543, upload-time = "2025-06-14T20:57:44.046Z" } wheels = [ @@ -3097,8 +3071,8 @@ name = "pyobjc-framework-latentsemanticmapping" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/8a/4e54ee2bc77d59d770b287daf73b629e2715a2b3b31264d164398131cbad/pyobjc_framework_latentsemanticmapping-11.1.tar.gz", hash = "sha256:c6c3142301e4d375c24a47dfaeebc2f3d0fc33128a1c0a755794865b9a371145", size = 17444, upload-time = "2025-06-14T20:57:44.643Z" } wheels = [ @@ -3110,8 +3084,8 @@ name = "pyobjc-framework-launchservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-coreservices" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreservices", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2b/0a/a76b13109b8ab563fdb2d7182ca79515f132f82ac6e1c52351a6b02896a8/pyobjc_framework_launchservices-11.1.tar.gz", hash = "sha256:80b55368b1e208d6c2c58395cc7bc12a630a2a402e00e4930493e9bace22b7bb", size = 20446, upload-time = "2025-06-14T20:57:45.258Z" } wheels = [ @@ -3123,8 +3097,8 @@ name = "pyobjc-framework-libdispatch" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/89/7830c293ba71feb086cb1551455757f26a7e2abd12f360d375aae32a4d7d/pyobjc_framework_libdispatch-11.1.tar.gz", hash = "sha256:11a704e50a0b7dbfb01552b7d686473ffa63b5254100fdb271a1fe368dd08e87", size = 53942, upload-time = "2025-06-14T20:57:45.903Z" } wheels = [ @@ -3137,8 +3111,8 @@ name = "pyobjc-framework-libxpc" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6a/c9/7e15e38ac23f5bfb4e82bdf3b7ef88e2f56a8b4ad884009bc2d5267d2e1f/pyobjc_framework_libxpc-11.1.tar.gz", hash = "sha256:8fd7468aa520ff19915f6d793070b84be1498cb87224bee2bad1f01d8375273a", size = 49135, upload-time = "2025-06-14T20:57:46.59Z" } wheels = [ @@ -3151,9 +3125,9 @@ name = "pyobjc-framework-linkpresentation" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/76/22873be73f12a3a11ae57af13167a1d2379e4e7eef584de137156a00f5ef/pyobjc_framework_linkpresentation-11.1.tar.gz", hash = "sha256:a785f393b01fdaada6d7d6d8de46b7173babba205b13b44f1dc884b3695c2fc9", size = 14987, upload-time = "2025-06-14T20:57:47.277Z" } wheels = [ @@ -3165,9 +3139,9 @@ name = "pyobjc-framework-localauthentication" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-security" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/27/9e3195f3561574140e9b9071a36f7e0ebd18f50ade9261d23b5b9df8fccd/pyobjc_framework_localauthentication-11.1.tar.gz", hash = "sha256:3cd48907c794bd414ac68b8ac595d83c7e1453b63fc2cfc2d2035b690d31eaa1", size = 40700, upload-time = "2025-06-14T20:57:47.931Z" } wheels = [ @@ -3180,9 +3154,9 @@ name = "pyobjc-framework-localauthenticationembeddedui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-localauthentication" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-localauthentication", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/7b/08c1e52487b07e9aee4c24a78f7c82a46695fa883113e3eece40f8e32d40/pyobjc_framework_localauthenticationembeddedui-11.1.tar.gz", hash = "sha256:22baf3aae606e5204e194f02bb205f244e27841ea7b4a4431303955475b4fa56", size = 14076, upload-time = "2025-06-14T20:57:48.557Z" } wheels = [ @@ -3194,8 +3168,8 @@ name = "pyobjc-framework-mailkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/7e/f22d733897e7618bd70a658b0353f5f897c583df04e7c5a2d68b99d43fbb/pyobjc_framework_mailkit-11.1.tar.gz", hash = "sha256:bf97dc44cb09b9eb9d591660dc0a41f077699976144b954caa4b9f0479211fd7", size = 32012, upload-time = "2025-06-14T20:57:49.173Z" } wheels = [ @@ -3207,10 +3181,10 @@ name = "pyobjc-framework-mapkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-corelocation" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corelocation", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/57/f0/505e074f49c783f2e65ca82174fd2d4348568f3f7281c1b81af816cf83bb/pyobjc_framework_mapkit-11.1.tar.gz", hash = "sha256:f3a5016f266091be313a118a42c0ea4f951c399b5259d93639eb643dacc626f1", size = 165614, upload-time = "2025-06-14T20:57:50.362Z" } wheels = [ @@ -3223,8 +3197,8 @@ name = "pyobjc-framework-mediaaccessibility" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8d/81/60412b423c121de0fa0aa3ef679825e1e2fe8b00fceddec7d72333ef564b/pyobjc_framework_mediaaccessibility-11.1.tar.gz", hash = "sha256:52479a998fec3d079d2d4590a945fc78c41fe7ac8c76f1964c9d8156880565a4", size = 18440, upload-time = "2025-06-14T20:57:51.126Z" } wheels = [ @@ -3236,10 +3210,10 @@ name = "pyobjc-framework-mediaextension" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-avfoundation" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-avfoundation", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/09/fd214dc0cf3f3bc3f528815af4799c0cb7b4bf4032703b19ea63486a132b/pyobjc_framework_mediaextension-11.1.tar.gz", hash = "sha256:85a1c8a94e9175fb364c453066ef99b95752343fd113f08a3805cad56e2fa709", size = 58489, upload-time = "2025-06-14T20:57:51.796Z" } wheels = [ @@ -3252,9 +3226,9 @@ name = "pyobjc-framework-medialibrary" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2b/06/11ff622fb5fbdd557998a45cedd2b0a1c7ea5cc6c5cb015dd6e42ebd1c41/pyobjc_framework_medialibrary-11.1.tar.gz", hash = "sha256:102f4326f789734b7b2dfe689abd3840ca75a76fb8058bd3e4f85398ae2ce29d", size = 18706, upload-time = "2025-06-14T20:57:52.474Z" } wheels = [ @@ -3266,8 +3240,8 @@ name = "pyobjc-framework-mediaplayer" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-avfoundation" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-avfoundation", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/80/d5/daba26eb8c70af1f3823acfd7925356acc4dd75eeac4fc86dc95d94d0e15/pyobjc_framework_mediaplayer-11.1.tar.gz", hash = "sha256:d07a634b98e1b9eedd82d76f35e616525da096bd341051ea74f0971e0f2f2ddd", size = 93749, upload-time = "2025-06-14T20:57:53.165Z" } wheels = [ @@ -3279,8 +3253,8 @@ name = "pyobjc-framework-mediatoolbox" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/68/cc230d2dfdeb974fdcfa828de655a43ce2bf4962023fd55bbb7ab0970100/pyobjc_framework_mediatoolbox-11.1.tar.gz", hash = "sha256:97834addc5179b3165c0d8cd74cc97ad43ed4c89547724216426348aca3b822a", size = 23568, upload-time = "2025-06-14T20:57:53.913Z" } wheels = [ @@ -3293,8 +3267,8 @@ name = "pyobjc-framework-metal" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/af/cf/29fea96fd49bf72946c5dac4c43ef50f26c15e9f76edd6f15580d556aa23/pyobjc_framework_metal-11.1.tar.gz", hash = "sha256:f9fd3b7574a824632ee9b7602973da30f172d2b575dd0c0f5ef76b44cfe9f6f9", size = 446549, upload-time = "2025-06-14T20:57:54.731Z" } wheels = [ @@ -3307,8 +3281,8 @@ name = "pyobjc-framework-metalfx" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-metal" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metal", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/10/20/4c839a356b534c161fb97e06589f418fc78cc5a0808362bdecf4f9a61a8d/pyobjc_framework_metalfx-11.1.tar.gz", hash = "sha256:555c1b895d4ba31be43930f45e219a5d7bb0e531d148a78b6b75b677cc588fd8", size = 27002, upload-time = "2025-06-14T20:57:55.949Z" } wheels = [ @@ -3321,9 +3295,9 @@ name = "pyobjc-framework-metalkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-metal" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metal", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/45/cb/7e01bc61625c7a6fea9c9888c9ed35aa6bbc47cda2fcd02b6525757bc2b8/pyobjc_framework_metalkit-11.1.tar.gz", hash = "sha256:8811cd81ee9583b9330df4f2499a73dcc53f3359cb92767b409acaec9e4faa1e", size = 45135, upload-time = "2025-06-14T20:57:56.601Z" } wheels = [ @@ -3336,8 +3310,8 @@ name = "pyobjc-framework-metalperformanceshaders" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-metal" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metal", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d0/11/5df398a158a6efe2c87ac5cae121ef2788242afe5d4302d703147b9fcd91/pyobjc_framework_metalperformanceshaders-11.1.tar.gz", hash = "sha256:8a312d090a0f51651e63d9001e6cc7c1aa04ceccf23b494cbf84b7fd3d122071", size = 302113, upload-time = "2025-06-14T20:57:57.407Z" } wheels = [ @@ -3350,8 +3324,8 @@ name = "pyobjc-framework-metalperformanceshadersgraph" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-metalperformanceshaders" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metalperformanceshaders", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/c3/8d98661f7eecd1f1b0d80a80961069081b88efd3a82fbbed2d7e6050c0ad/pyobjc_framework_metalperformanceshadersgraph-11.1.tar.gz", hash = "sha256:d25225aab4edc6f786b29fe3d9badc4f3e2d0caeab1054cd4f224258c1b6dbe2", size = 105098, upload-time = "2025-06-14T20:57:58.273Z" } wheels = [ @@ -3363,8 +3337,8 @@ name = "pyobjc-framework-metrickit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/48/8ae969a51a91864000e39c1de74627b12ff587b1dbad9406f7a30dfe71f8/pyobjc_framework_metrickit-11.1.tar.gz", hash = "sha256:a79d37575489916c35840e6a07edd958be578d3be7a3d621684d028d721f0b85", size = 40952, upload-time = "2025-06-14T20:57:58.996Z" } wheels = [ @@ -3377,8 +3351,8 @@ name = "pyobjc-framework-mlcompute" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8b/e6/f064dec650fb1209f41aba0c3074416cb9b975a7cf4d05d93036e3d917f0/pyobjc_framework_mlcompute-11.1.tar.gz", hash = "sha256:f6c4c3ea6a62e4e3927abf9783c40495aa8bb9a8c89def744b0822da58c2354b", size = 89021, upload-time = "2025-06-14T20:57:59.997Z" } wheels = [ @@ -3390,9 +3364,9 @@ name = "pyobjc-framework-modelio" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a0/27/140bf75706332729de252cc4141e8c8afe16a0e9e5818b5a23155aa3473c/pyobjc_framework_modelio-11.1.tar.gz", hash = "sha256:fad0fa2c09d468ac7e49848e144f7bbce6826f2178b3120add8960a83e5bfcb7", size = 123203, upload-time = "2025-06-14T20:58:01.035Z" } wheels = [ @@ -3405,8 +3379,8 @@ name = "pyobjc-framework-multipeerconnectivity" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/73/99/75bf6170e282d9e546b353b65af7859de8b1b27ddc431fc4afbf15423d01/pyobjc_framework_multipeerconnectivity-11.1.tar.gz", hash = "sha256:a3dacca5e6e2f1960dd2d1107d98399ff81ecf54a9852baa8ec8767dbfdbf54b", size = 26149, upload-time = "2025-06-14T20:58:01.793Z" } wheels = [ @@ -3419,8 +3393,8 @@ name = "pyobjc-framework-naturallanguage" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a2/e9/5352fbf09c5d5360405dea49fb77e53ed55acd572a94ce9a0d05f64d2b70/pyobjc_framework_naturallanguage-11.1.tar.gz", hash = "sha256:ab1fc711713aa29c32719774fc623bf2d32168aed21883970d4896e901ff4b41", size = 46120, upload-time = "2025-06-14T20:58:02.808Z" } wheels = [ @@ -3432,8 +3406,8 @@ name = "pyobjc-framework-netfs" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/68/5d/d68cc59a1c1ea61f227ed58e7b185a444d560655320b53ced155076f5b78/pyobjc_framework_netfs-11.1.tar.gz", hash = "sha256:9c49f050c8171dc37e54d05dd12a63979c8b6b565c10f05092923a2250446f50", size = 15910, upload-time = "2025-06-14T20:58:03.811Z" } wheels = [ @@ -3445,8 +3419,8 @@ name = "pyobjc-framework-network" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0a/ee/5ea93e48eca341b274027e1532bd8629fd55d609cd9c39c2c3acf26158c3/pyobjc_framework_network-11.1.tar.gz", hash = "sha256:f6df7a58a1279bbc976fd7e2efe813afbbb18427df40463e6e2ee28fba07d2df", size = 124670, upload-time = "2025-06-14T20:58:05.491Z" } wheels = [ @@ -3459,8 +3433,8 @@ name = "pyobjc-framework-networkextension" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/30/d1eee738d702bbca78effdaa346a2b05359ab8a96d961b7cb44838e236ca/pyobjc_framework_networkextension-11.1.tar.gz", hash = "sha256:2b74b430ca651293e5aa90a1e7571b200d0acbf42803af87306ac8a1c70b0d4b", size = 217252, upload-time = "2025-06-14T20:58:06.311Z" } wheels = [ @@ -3473,8 +3447,8 @@ name = "pyobjc-framework-notificationcenter" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/4a/d3529b9bd7aae2c89d258ebc234673c5435e217a5136abd8c0aba37b916b/pyobjc_framework_notificationcenter-11.1.tar.gz", hash = "sha256:0b938053f2d6b1cea9db79313639d7eb9ddd5b2a5436a346be0887e75101e717", size = 23389, upload-time = "2025-06-14T20:58:07.136Z" } wheels = [ @@ -3487,8 +3461,8 @@ name = "pyobjc-framework-opendirectory" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/02/ac56c56fdfbc24cdf87f4a624f81bbe2e371d0983529b211a18c6170e932/pyobjc_framework_opendirectory-11.1.tar.gz", hash = "sha256:319ac3424ed0350be458b78148914468a8fc13a069d62e7869e3079108e4f118", size = 188880, upload-time = "2025-06-14T20:58:08.003Z" } wheels = [ @@ -3500,8 +3474,8 @@ name = "pyobjc-framework-osakit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/56/22/f9cdfb5de255b335f99e61a3284be7cb1552a43ed1dfe7c22cc868c23819/pyobjc_framework_osakit-11.1.tar.gz", hash = "sha256:920987da78b67578367c315d208f87e8fab01dd35825d72242909f29fb43c820", size = 22290, upload-time = "2025-06-14T20:58:09.103Z" } wheels = [ @@ -3513,10 +3487,10 @@ name = "pyobjc-framework-oslog" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/79/93/3feb7f6150b50165524750a424f5434448392123420cb4673db766c3f54a/pyobjc_framework_oslog-11.1.tar.gz", hash = "sha256:b2af409617e6b68fa1f1467c5a5679ebf59afd0cdc4b4528e1616059959a7979", size = 24689, upload-time = "2025-06-14T20:58:09.739Z" } wheels = [ @@ -3529,8 +3503,8 @@ name = "pyobjc-framework-passkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/05/063db500e7df70e39cbb5518a5a03c2acc06a1ca90b057061daea00129f3/pyobjc_framework_passkit-11.1.tar.gz", hash = "sha256:d2408b58960fca66607b483353c1ffbd751ef0bef394a1853ec414a34029566f", size = 144859, upload-time = "2025-06-14T20:58:10.761Z" } wheels = [ @@ -3543,8 +3517,8 @@ name = "pyobjc-framework-pencilkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/75/d0/bbbe9dadcfc37e33a63d43b381a8d9a64eca27559df38efb74d524fa6260/pyobjc_framework_pencilkit-11.1.tar.gz", hash = "sha256:9c173e0fe70179feadc3558de113a8baad61b584fe70789b263af202bfa4c6be", size = 22570, upload-time = "2025-06-14T20:58:11.538Z" } wheels = [ @@ -3556,8 +3530,8 @@ name = "pyobjc-framework-phase" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-avfoundation" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-avfoundation", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c6/d2/e9384b5b3fbcc79e8176cb39fcdd48b77f60cd1cb64f9ee4353762b037dc/pyobjc_framework_phase-11.1.tar.gz", hash = "sha256:a940d81ac5c393ae3da94144cf40af33932e0a9731244e2cfd5c9c8eb851e3fc", size = 58986, upload-time = "2025-06-14T20:58:12.196Z" } wheels = [ @@ -3569,8 +3543,8 @@ name = "pyobjc-framework-photos" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b0/576652ecd05c26026ab4e75e0d81466edd570d060ce7df3d6bd812eb90d0/pyobjc_framework_photos-11.1.tar.gz", hash = "sha256:c8c3b25b14a2305047f72c7c081ff3655b3d051f7ed531476c03246798f8156d", size = 92569, upload-time = "2025-06-14T20:58:12.939Z" } wheels = [ @@ -3583,8 +3557,8 @@ name = "pyobjc-framework-photosui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/20/bb/e6de720efde2e9718677c95c6ae3f97047be437cda7a0f050cd1d6d2a434/pyobjc_framework_photosui-11.1.tar.gz", hash = "sha256:1c7ffab4860ce3e2b50feeed4f1d84488a9e38546db0bec09484d8d141c650df", size = 48443, upload-time = "2025-06-14T20:58:13.626Z" } wheels = [ @@ -3597,8 +3571,8 @@ name = "pyobjc-framework-preferencepanes" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/34/ac/9324602daf9916308ebf1935b8a4b91c93b9ae993dcd0da731c0619c2836/pyobjc_framework_preferencepanes-11.1.tar.gz", hash = "sha256:6e4a55195ec9fc921e0eaad6b3038d0ab91f0bb2f39206aa6fccd24b14a0f1d8", size = 26212, upload-time = "2025-06-14T20:58:14.361Z" } wheels = [ @@ -3610,8 +3584,8 @@ name = "pyobjc-framework-pushkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/f0/92d0eb26bf8af8ebf6b5b88df77e70b807de11f01af0162e0a429fcfb892/pyobjc_framework_pushkit-11.1.tar.gz", hash = "sha256:540769a4aadc3c9f08beca8496fe305372501eb28fdbca078db904a07b8e10f4", size = 21362, upload-time = "2025-06-14T20:58:15.642Z" } wheels = [ @@ -3624,8 +3598,8 @@ name = "pyobjc-framework-quartz" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/ac/6308fec6c9ffeda9942fef72724f4094c6df4933560f512e63eac37ebd30/pyobjc_framework_quartz-11.1.tar.gz", hash = "sha256:a57f35ccfc22ad48c87c5932818e583777ff7276605fef6afad0ac0741169f75", size = 3953275, upload-time = "2025-06-14T20:58:17.924Z" } wheels = [ @@ -3638,9 +3612,9 @@ name = "pyobjc-framework-quicklookthumbnailing" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/aa/98/6e87f360c2dfc870ae7870b8a25fdea8ddf1d62092c755686cebe7ec1a07/pyobjc_framework_quicklookthumbnailing-11.1.tar.gz", hash = "sha256:1614dc108c1d45bbf899ea84b8691288a5b1d25f2d6f0c57dfffa962b7a478c3", size = 16527, upload-time = "2025-06-14T20:58:20.811Z" } wheels = [ @@ -3652,8 +3626,8 @@ name = "pyobjc-framework-replaykit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c8/4f/014e95f0fd6842d7fcc3d443feb6ee65ac69d06c66ffa9327fc33ceb7c27/pyobjc_framework_replaykit-11.1.tar.gz", hash = "sha256:6919baa123a6d8aad769769fcff87369e13ee7bae11b955a8185a406a651061b", size = 26132, upload-time = "2025-06-14T20:58:21.853Z" } wheels = [ @@ -3666,8 +3640,8 @@ name = "pyobjc-framework-safariservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/fc/c47d2abf3c1de6db21d685cace76a0931d594aa369e3d090260295273f6e/pyobjc_framework_safariservices-11.1.tar.gz", hash = "sha256:39a17df1a8e1c339457f3acbff0dc0eae4681d158f9d783a11995cf484aa9cd0", size = 34905, upload-time = "2025-06-14T20:58:22.492Z" } wheels = [ @@ -3680,9 +3654,9 @@ name = "pyobjc-framework-safetykit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/cc/f6aa5d6f45179bd084416511be4e5b0dd0752cb76daa93869e6edb806096/pyobjc_framework_safetykit-11.1.tar.gz", hash = "sha256:c6b44e0cf69e27584ac3ef3d8b771d19a7c2ccd9c6de4138d091358e036322d4", size = 21240, upload-time = "2025-06-14T20:58:23.132Z" } wheels = [ @@ -3695,9 +3669,9 @@ name = "pyobjc-framework-scenekit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/64/cf/2d89777120d2812e7ee53c703bf6fc8968606c29ddc1351bc63f0a2a5692/pyobjc_framework_scenekit-11.1.tar.gz", hash = "sha256:82941f1e5040114d6e2c9fd35507244e102ef561c637686091b71a7ad0f31306", size = 214118, upload-time = "2025-06-14T20:58:24.003Z" } wheels = [ @@ -3710,9 +3684,9 @@ name = "pyobjc-framework-screencapturekit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/a5/9bd1f1ad1773a1304ccde934ff39e0f0a0b0034441bf89166aea649606de/pyobjc_framework_screencapturekit-11.1.tar.gz", hash = "sha256:11443781a30ed446f2d892c9e6642ca4897eb45f1a1411136ca584997fa739e0", size = 53548, upload-time = "2025-06-14T20:58:24.837Z" } wheels = [ @@ -3725,8 +3699,8 @@ name = "pyobjc-framework-screensaver" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7c/f6/f2d48583b29fc67b64aa1f415fd51faf003d045cdb1f3acab039b9a3f59f/pyobjc_framework_screensaver-11.1.tar.gz", hash = "sha256:d5fbc9dc076cc574ead183d521840b56be0c160415e43cb8e01cfddd6d6372c2", size = 24302, upload-time = "2025-06-14T20:58:25.52Z" } wheels = [ @@ -3739,8 +3713,8 @@ name = "pyobjc-framework-screentime" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/82/33/ebed70a1de134de936bb9a12d5c76f24e1e335ff4964f9bb0af9b09607f1/pyobjc_framework_screentime-11.1.tar.gz", hash = "sha256:9bb8269456bbb674e1421182efe49f9168ceefd4e7c497047c7bf63e2f510a34", size = 14875, upload-time = "2025-06-14T20:58:26.179Z" } wheels = [ @@ -3752,8 +3726,8 @@ name = "pyobjc-framework-scriptingbridge" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8e/c1/5b1dd01ff173df4c6676f97405113458918819cb2064c1735b61948e8800/pyobjc_framework_scriptingbridge-11.1.tar.gz", hash = "sha256:604445c759210a35d86d3e0dfcde0aac8e5e3e9d9e35759e0723952138843699", size = 23155, upload-time = "2025-06-14T20:58:26.812Z" } wheels = [ @@ -3766,8 +3740,8 @@ name = "pyobjc-framework-searchkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-coreservices" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreservices", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6e/20/61b73fddae0d1a94f5defb0cd4b4f391ec03bfcce7ebe830cb827d5e208a/pyobjc_framework_searchkit-11.1.tar.gz", hash = "sha256:13a194eefcf1359ce9972cd92f2aadddf103f3efb1b18fd578ba5367dff3c10c", size = 30918, upload-time = "2025-06-14T20:58:27.447Z" } wheels = [ @@ -3779,8 +3753,8 @@ name = "pyobjc-framework-security" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ee/6f/ba50ed2d9c1192c67590a7cfefa44fc5f85c776d1e25beb224dec32081f6/pyobjc_framework_security-11.1.tar.gz", hash = "sha256:dabcee6987c6bae575e2d1ef0fcbe437678c4f49f1c25a4b131a5e960f31a2da", size = 302291, upload-time = "2025-06-14T20:58:28.506Z" } wheels = [ @@ -3793,9 +3767,9 @@ name = "pyobjc-framework-securityfoundation" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-security" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/d4/19591dd0938a45b6d8711ef9ae5375b87c37a55b45d79c52d6f83a8d991f/pyobjc_framework_securityfoundation-11.1.tar.gz", hash = "sha256:b3c4cf70735a93e9df40f3a14478143959c415778f27be8c0dc9ae0c5b696b92", size = 13270, upload-time = "2025-06-14T20:58:29.304Z" } wheels = [ @@ -3807,9 +3781,9 @@ name = "pyobjc-framework-securityinterface" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-security" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/be/c846651c3e7f38a637c40ae1bcda9f14237c2395637c3a188df4f733c727/pyobjc_framework_securityinterface-11.1.tar.gz", hash = "sha256:e7aa6373e525f3ae05d71276e821a6348c53fec9f812b90eec1dbadfcb507bc9", size = 37648, upload-time = "2025-06-14T20:58:29.932Z" } wheels = [ @@ -3822,9 +3796,9 @@ name = "pyobjc-framework-securityui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-security" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/07/5b/3b5585d56e0bcaba82e0661224bbc7aaf29fba6b10498971dbe08b2b490a/pyobjc_framework_securityui-11.1.tar.gz", hash = "sha256:e80c93e8a56bf89e4c0333047b9f8219752dd6de290681e9e2e2b2e26d69e92d", size = 12179, upload-time = "2025-06-14T20:58:30.928Z" } wheels = [ @@ -3836,9 +3810,9 @@ name = "pyobjc-framework-sensitivecontentanalysis" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/56/7b/e28f6b30d99e9d464427a07ada82b33cd3292f310bf478a1824051d066b9/pyobjc_framework_sensitivecontentanalysis-11.1.tar.gz", hash = "sha256:5b310515c7386f7afaf13e4632d7d9590688182bb7b563f8026c304bdf317308", size = 12796, upload-time = "2025-06-14T20:58:31.488Z" } wheels = [ @@ -3850,8 +3824,8 @@ name = "pyobjc-framework-servicemanagement" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/20/c6/32e11599d9d232311607b79eb2d1d21c52eaaf001599ea85f8771a933fa2/pyobjc_framework_servicemanagement-11.1.tar.gz", hash = "sha256:90a07164da49338480e0e135b445acc6ae7c08549a2037d1e512d2605fedd80a", size = 16645, upload-time = "2025-06-14T20:58:32.062Z" } wheels = [ @@ -3863,8 +3837,8 @@ name = "pyobjc-framework-sharedwithyou" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-sharedwithyoucore" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-sharedwithyoucore", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fe/a5/e299fbd0c13d4fac9356459f21372f6eef4279d0fbc99ba316d88dfbbfb4/pyobjc_framework_sharedwithyou-11.1.tar.gz", hash = "sha256:ece3a28a3083d0bcad0ac95b01f0eb699b9d2d0c02c61305bfd402678753ff6e", size = 34216, upload-time = "2025-06-14T20:58:32.75Z" } wheels = [ @@ -3877,8 +3851,8 @@ name = "pyobjc-framework-sharedwithyoucore" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/79/a3/1ca6ff1b785772c7c5a38a7c017c6f971b1eda638d6a0aab3bbde18ac086/pyobjc_framework_sharedwithyoucore-11.1.tar.gz", hash = "sha256:790050d25f47bda662a9f008b17ca640ac2460f2559a56b17995e53f2f44ed73", size = 29459, upload-time = "2025-06-14T20:58:33.422Z" } wheels = [ @@ -3891,8 +3865,8 @@ name = "pyobjc-framework-shazamkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/08/ba739b97f1e441653bae8da5dd1e441bbbfa43940018d21edb60da7dd163/pyobjc_framework_shazamkit-11.1.tar.gz", hash = "sha256:c6e3c9ab8744d9319a89b78ae6f185bb5704efb68509e66d77bcd1f84a9446d6", size = 25797, upload-time = "2025-06-14T20:58:34.086Z" } wheels = [ @@ -3905,8 +3879,8 @@ name = "pyobjc-framework-social" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/07/2e/cc7707b7a40df392c579087947049f3e1f0e00597e7151ec411f654d8bef/pyobjc_framework_social-11.1.tar.gz", hash = "sha256:fbc09d7b00dad45b547f9b2329f4dcee3f5a50e2348de1870de0bd7be853a5b7", size = 14540, upload-time = "2025-06-14T20:58:35.116Z" } wheels = [ @@ -3918,8 +3892,8 @@ name = "pyobjc-framework-soundanalysis" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/d4/b9497dbb57afdf0d22f61bb6e776a6f46cf9294c890448acde5b46dd61f3/pyobjc_framework_soundanalysis-11.1.tar.gz", hash = "sha256:42cd25b7e0f343d8b59367f72b5dae96cf65696bdb8eeead8d7424ed37aa1434", size = 16539, upload-time = "2025-06-14T20:58:35.813Z" } wheels = [ @@ -3931,8 +3905,8 @@ name = "pyobjc-framework-speech" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/76/2a1fd7637b2c662349ede09806e159306afeebfba18fb062ad053b41d811/pyobjc_framework_speech-11.1.tar.gz", hash = "sha256:d382977208c3710eacea89e05eae4578f1638bb5a7b667c06971e3d34e96845c", size = 41179, upload-time = "2025-06-14T20:58:36.43Z" } wheels = [ @@ -3945,9 +3919,9 @@ name = "pyobjc-framework-spritekit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/02/2e253ba4f7fad6efe05fd5fcf44aede093f6c438d608d67c6c6623a1846d/pyobjc_framework_spritekit-11.1.tar.gz", hash = "sha256:914da6e846573cac8db5e403dec9a3e6f6edf5211f9b7e429734924d00f65108", size = 130297, upload-time = "2025-06-14T20:58:37.113Z" } wheels = [ @@ -3960,8 +3934,8 @@ name = "pyobjc-framework-storekit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/a0/58cab9ebc9ac9282e1d4734b1987d1c3cd652b415ec3e678fcc5e735d279/pyobjc_framework_storekit-11.1.tar.gz", hash = "sha256:85acc30c0bfa120b37c3c5ac693fe9ad2c2e351ee7a1f9ea6f976b0c311ff164", size = 76421, upload-time = "2025-06-14T20:58:37.86Z" } wheels = [ @@ -3974,8 +3948,8 @@ name = "pyobjc-framework-symbols" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/af/7191276204bd3e7db1d0a3e490a869956606f77f7a303a04d92a5d0c3f7b/pyobjc_framework_symbols-11.1.tar.gz", hash = "sha256:0e09b7813ef2ebdca7567d3179807444dd60f3f393202b35b755d4e1baf99982", size = 13377, upload-time = "2025-06-14T20:58:38.542Z" } wheels = [ @@ -3987,9 +3961,9 @@ name = "pyobjc-framework-syncservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coredata" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coredata", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/69/45/cd9fa83ed1d75be7130fb8e41c375f05b5d6621737ec37e9d8da78676613/pyobjc_framework_syncservices-11.1.tar.gz", hash = "sha256:0f141d717256b98c17ec2eddbc983c4bd39dfa00dc0c31b4174742e73a8447fe", size = 57996, upload-time = "2025-06-14T20:58:39.146Z" } wheels = [ @@ -4002,8 +3976,8 @@ name = "pyobjc-framework-systemconfiguration" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e2/3d/41590c0afc72e93d911348fbde0c9c1071ff53c6f86df42df64b21174bb9/pyobjc_framework_systemconfiguration-11.1.tar.gz", hash = "sha256:f30ed0e9a8233fecb06522e67795918ab230ddcc4a18e15494eff7532f4c3ae1", size = 143410, upload-time = "2025-06-14T20:58:39.917Z" } wheels = [ @@ -4016,8 +3990,8 @@ name = "pyobjc-framework-systemextensions" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/57/4609fd9183383616b1e643c2489ad774335f679523a974b9ce346a6d4d5b/pyobjc_framework_systemextensions-11.1.tar.gz", hash = "sha256:8ff9f0aad14dcdd07dd47545c1dd20df7a286306967b0a0232c81fcc382babe6", size = 23062, upload-time = "2025-06-14T20:58:40.686Z" } wheels = [ @@ -4030,8 +4004,8 @@ name = "pyobjc-framework-threadnetwork" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e7/a4/5400a222ced0e4f077a8f4dd0188e08e2af4762e72ed0ed39f9d27feefc9/pyobjc_framework_threadnetwork-11.1.tar.gz", hash = "sha256:73a32782f44b61ca0f8a4a9811c36b1ca1cdcf96c8a3ba4de35d8e8e58a86ad5", size = 13572, upload-time = "2025-06-14T20:58:41.311Z" } wheels = [ @@ -4043,8 +4017,8 @@ name = "pyobjc-framework-uniformtypeidentifiers" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/4f/066ed1c69352ccc29165f45afb302f8c9c2b5c6f33ee3abfa41b873c07e5/pyobjc_framework_uniformtypeidentifiers-11.1.tar.gz", hash = "sha256:86c499bec8953aeb0c95af39b63f2592832384f09f12523405650b5d5f1ed5e9", size = 20599, upload-time = "2025-06-14T20:58:41.945Z" } wheels = [ @@ -4056,8 +4030,8 @@ name = "pyobjc-framework-usernotifications" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/4c/e7e180fcd06c246c37f218bcb01c40ea0213fde5ace3c09d359e60dcaafd/pyobjc_framework_usernotifications-11.1.tar.gz", hash = "sha256:38fc763afa7854b41ddfca8803f679a7305d278af8a7ad02044adc1265699996", size = 55428, upload-time = "2025-06-14T20:58:42.572Z" } wheels = [ @@ -4070,9 +4044,9 @@ name = "pyobjc-framework-usernotificationsui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-usernotifications" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-usernotifications", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d2/c4/03d97bd3adcee9b857533cb42967df0d019f6a034adcdbcfca2569d415b2/pyobjc_framework_usernotificationsui-11.1.tar.gz", hash = "sha256:18e0182bddd10381884530d6a28634ebb3280912592f8f2ad5bac2a9308c6a65", size = 14123, upload-time = "2025-06-14T20:58:43.267Z" } wheels = [ @@ -4084,8 +4058,8 @@ name = "pyobjc-framework-videosubscriberaccount" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/aa/00/cd9d93d06204bbb7fe68fb97022b0dd4ecdf8af3adb6d70a41e22c860d55/pyobjc_framework_videosubscriberaccount-11.1.tar.gz", hash = "sha256:2dd78586260fcee51044e129197e8bf2e157176e02babeec2f873afa4235d8c6", size = 28856, upload-time = "2025-06-14T20:58:43.903Z" } wheels = [ @@ -4097,10 +4071,10 @@ name = "pyobjc-framework-videotoolbox" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/e3/df9096f54ae1f27cab8f922ee70cbda5d80f8c1d12734c38580829858133/pyobjc_framework_videotoolbox-11.1.tar.gz", hash = "sha256:a27985656e1b639cdb102fcc727ebc39f71bb1a44cdb751c8c80cc9fe938f3a9", size = 88551, upload-time = "2025-06-14T20:58:44.566Z" } wheels = [ @@ -4113,8 +4087,8 @@ name = "pyobjc-framework-virtualization" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/ff/57214e8f42755eeaad516a7e673dae4341b8742005d368ecc22c7a790b0b/pyobjc_framework_virtualization-11.1.tar.gz", hash = "sha256:4221ee5eb669e43a2ff46e04178bec149af2d65205deb5d4db5fa62ea060e022", size = 78633, upload-time = "2025-06-14T20:58:45.358Z" } wheels = [ @@ -4127,10 +4101,10 @@ name = "pyobjc-framework-vision" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coreml" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreml", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/a8/7128da4d0a0103cabe58910a7233e2f98d18c590b1d36d4b3efaaedba6b9/pyobjc_framework_vision-11.1.tar.gz", hash = "sha256:26590512ee7758da3056499062a344b8a351b178be66d4b719327884dde4216b", size = 133721, upload-time = "2025-06-14T20:58:46.095Z" } wheels = [ @@ -4143,8 +4117,8 @@ name = "pyobjc-framework-webkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/92/04/fb3d0b68994f7e657ef00c1ac5fc1c04ae2fc7ea581d647f5ae1f6739b14/pyobjc_framework_webkit-11.1.tar.gz", hash = "sha256:27e701c7aaf4f24fc7e601a128e2ef14f2773f4ab071b9db7438dc5afb5053ae", size = 717102, upload-time = "2025-06-14T20:58:47.461Z" } wheels = [ @@ -4335,8 +4309,8 @@ wheels = [ [[package]] name = "pytest-xdist" -version = "3.7.1.dev3+g909e97b" -source = { git = "https://github.com/sshane/pytest-xdist?rev=909e97b49d12401c10608f9d777bfc9dab8a4413#909e97b49d12401c10608f9d777bfc9dab8a4413" } +version = "3.7.1.dev24+g2b4372b" +source = { git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da#2b4372bd62699fb412c4fe2f95bf9f01bd2018da" } dependencies = [ { name = "execnet" }, { name = "pytest" }, @@ -4359,7 +4333,7 @@ name = "python-xlib" version = "0.33" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six" }, + { name = "six", marker = "sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068, upload-time = "2022-12-25T18:53:00.824Z" } wheels = [ @@ -4477,38 +4451,38 @@ wheels = [ [[package]] name = "pyzmq" -version = "27.0.0" +version = "27.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/06/50a4e9648b3e8b992bef8eb632e457307553a89d294103213cfd47b3da69/pyzmq-27.0.0.tar.gz", hash = "sha256:b1f08eeb9ce1510e6939b6e5dcd46a17765e2333daae78ecf4606808442e52cf", size = 280478, upload-time = "2025-06-13T14:09:07.087Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/5f/557d2032a2f471edbcc227da724c24a1c05887b5cda1e3ae53af98b9e0a5/pyzmq-27.0.1.tar.gz", hash = "sha256:45c549204bc20e7484ffd2555f6cf02e572440ecf2f3bdd60d4404b20fddf64b", size = 281158, upload-time = "2025-08-03T05:05:40.352Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/df/84c630654106d9bd9339cdb564aa941ed41b023a0264251d6743766bb50e/pyzmq-27.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:21457825249b2a53834fa969c69713f8b5a79583689387a5e7aed880963ac564", size = 1332718, upload-time = "2025-06-13T14:07:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/c1/8e/f6a5461a07654d9840d256476434ae0ff08340bba562a455f231969772cb/pyzmq-27.0.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1958947983fef513e6e98eff9cb487b60bf14f588dc0e6bf35fa13751d2c8251", size = 908248, upload-time = "2025-06-13T14:07:18.033Z" }, - { url = "https://files.pythonhosted.org/packages/7c/93/82863e8d695a9a3ae424b63662733ae204a295a2627d52af2f62c2cd8af9/pyzmq-27.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0dc628b5493f9a8cd9844b8bee9732ef587ab00002157c9329e4fc0ef4d3afa", size = 668647, upload-time = "2025-06-13T14:07:19.378Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/15278769b348121eacdbfcbd8c4d40f1102f32fa6af5be1ffc032ed684be/pyzmq-27.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7bbe9e1ed2c8d3da736a15694d87c12493e54cc9dc9790796f0321794bbc91f", size = 856600, upload-time = "2025-06-13T14:07:20.906Z" }, - { url = "https://files.pythonhosted.org/packages/d4/af/1c469b3d479bd095edb28e27f12eee10b8f00b356acbefa6aeb14dd295d1/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc1091f59143b471d19eb64f54bae4f54bcf2a466ffb66fe45d94d8d734eb495", size = 1657748, upload-time = "2025-06-13T14:07:22.549Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f4/17f965d0ee6380b1d6326da842a50e4b8b9699745161207945f3745e8cb5/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7011ade88c8e535cf140f8d1a59428676fbbce7c6e54fefce58bf117aefb6667", size = 2034311, upload-time = "2025-06-13T14:07:23.966Z" }, - { url = "https://files.pythonhosted.org/packages/e0/6e/7c391d81fa3149fd759de45d298003de6cfab343fb03e92c099821c448db/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c386339d7e3f064213aede5d03d054b237937fbca6dd2197ac8cf3b25a6b14e", size = 1893630, upload-time = "2025-06-13T14:07:25.899Z" }, - { url = "https://files.pythonhosted.org/packages/0e/e0/eaffe7a86f60e556399e224229e7769b717f72fec0706b70ab2c03aa04cb/pyzmq-27.0.0-cp311-cp311-win32.whl", hash = "sha256:0546a720c1f407b2172cb04b6b094a78773491497e3644863cf5c96c42df8cff", size = 567706, upload-time = "2025-06-13T14:07:27.595Z" }, - { url = "https://files.pythonhosted.org/packages/c9/05/89354a8cffdcce6e547d48adaaf7be17007fc75572123ff4ca90a4ca04fc/pyzmq-27.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:15f39d50bd6c9091c67315ceb878a4f531957b121d2a05ebd077eb35ddc5efed", size = 630322, upload-time = "2025-06-13T14:07:28.938Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/4ab976d5e1e63976719389cc4f3bfd248a7f5f2bb2ebe727542363c61b5f/pyzmq-27.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c5817641eebb391a2268c27fecd4162448e03538387093cdbd8bf3510c316b38", size = 558435, upload-time = "2025-06-13T14:07:30.256Z" }, - { url = "https://files.pythonhosted.org/packages/93/a7/9ad68f55b8834ede477842214feba6a4c786d936c022a67625497aacf61d/pyzmq-27.0.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:cbabc59dcfaac66655c040dfcb8118f133fb5dde185e5fc152628354c1598e52", size = 1305438, upload-time = "2025-06-13T14:07:31.676Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ee/26aa0f98665a22bc90ebe12dced1de5f3eaca05363b717f6fb229b3421b3/pyzmq-27.0.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cb0ac5179cba4b2f94f1aa208fbb77b62c4c9bf24dd446278b8b602cf85fcda3", size = 895095, upload-time = "2025-06-13T14:07:33.104Z" }, - { url = "https://files.pythonhosted.org/packages/cf/85/c57e7ab216ecd8aa4cc7e3b83b06cc4e9cf45c87b0afc095f10cd5ce87c1/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53a48f0228eab6cbf69fde3aa3c03cbe04e50e623ef92ae395fce47ef8a76152", size = 651826, upload-time = "2025-06-13T14:07:34.831Z" }, - { url = "https://files.pythonhosted.org/packages/69/9a/9ea7e230feda9400fb0ae0d61d7d6ddda635e718d941c44eeab22a179d34/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111db5f395e09f7e775f759d598f43cb815fc58e0147623c4816486e1a39dc22", size = 839750, upload-time = "2025-06-13T14:07:36.553Z" }, - { url = "https://files.pythonhosted.org/packages/08/66/4cebfbe71f3dfbd417011daca267539f62ed0fbc68105357b68bbb1a25b7/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c8878011653dcdc27cc2c57e04ff96f0471e797f5c19ac3d7813a245bcb24371", size = 1641357, upload-time = "2025-06-13T14:07:38.21Z" }, - { url = "https://files.pythonhosted.org/packages/ac/f6/b0f62578c08d2471c791287149cb8c2aaea414ae98c6e995c7dbe008adfb/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:c0ed2c1f335ba55b5fdc964622254917d6b782311c50e138863eda409fbb3b6d", size = 2020281, upload-time = "2025-06-13T14:07:39.599Z" }, - { url = "https://files.pythonhosted.org/packages/37/b9/4f670b15c7498495da9159edc374ec09c88a86d9cd5a47d892f69df23450/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e918d70862d4cfd4b1c187310015646a14e1f5917922ab45b29f28f345eeb6be", size = 1877110, upload-time = "2025-06-13T14:07:41.027Z" }, - { url = "https://files.pythonhosted.org/packages/66/31/9dee25c226295b740609f0d46db2fe972b23b6f5cf786360980524a3ba92/pyzmq-27.0.0-cp312-abi3-win32.whl", hash = "sha256:88b4e43cab04c3c0f0d55df3b1eef62df2b629a1a369b5289a58f6fa8b07c4f4", size = 559297, upload-time = "2025-06-13T14:07:42.533Z" }, - { url = "https://files.pythonhosted.org/packages/9b/12/52da5509800f7ff2d287b2f2b4e636e7ea0f001181cba6964ff6c1537778/pyzmq-27.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:dce4199bf5f648a902ce37e7b3afa286f305cd2ef7a8b6ec907470ccb6c8b371", size = 619203, upload-time = "2025-06-13T14:07:43.843Z" }, - { url = "https://files.pythonhosted.org/packages/93/6d/7f2e53b19d1edb1eb4f09ec7c3a1f945ca0aac272099eab757d15699202b/pyzmq-27.0.0-cp312-abi3-win_arm64.whl", hash = "sha256:56e46bbb85d52c1072b3f809cc1ce77251d560bc036d3a312b96db1afe76db2e", size = 551927, upload-time = "2025-06-13T14:07:45.51Z" }, - { url = "https://files.pythonhosted.org/packages/98/a6/92394373b8dbc1edc9d53c951e8d3989d518185174ee54492ec27711779d/pyzmq-27.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd1dc59763effd1576f8368047c9c31468fce0af89d76b5067641137506792ae", size = 835948, upload-time = "2025-06-13T14:08:43.516Z" }, - { url = "https://files.pythonhosted.org/packages/56/f3/4dc38d75d9995bfc18773df3e41f2a2ca9b740b06f1a15dbf404077e7588/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:60e8cc82d968174650c1860d7b716366caab9973787a1c060cf8043130f7d0f7", size = 799874, upload-time = "2025-06-13T14:08:45.017Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ba/64af397e0f421453dc68e31d5e0784d554bf39013a2de0872056e96e58af/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14fe7aaac86e4e93ea779a821967360c781d7ac5115b3f1a171ced77065a0174", size = 567400, upload-time = "2025-06-13T14:08:46.855Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/ec956cbe98809270b59a22891d5758edae147a258e658bf3024a8254c855/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ad0562d4e6abb785be3e4dd68599c41be821b521da38c402bc9ab2a8e7ebc7e", size = 747031, upload-time = "2025-06-13T14:08:48.419Z" }, - { url = "https://files.pythonhosted.org/packages/be/8a/4a3764a68abc02e2fbb0668d225b6fda5cd39586dd099cee8b2ed6ab0452/pyzmq-27.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9df43a2459cd3a3563404c1456b2c4c69564daa7dbaf15724c09821a3329ce46", size = 544726, upload-time = "2025-06-13T14:08:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/ae/18/a8e0da6ababbe9326116fb1c890bf1920eea880e8da621afb6bc0f39a262/pyzmq-27.0.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:9729190bd770314f5fbba42476abf6abe79a746eeda11d1d68fd56dd70e5c296", size = 1332721, upload-time = "2025-08-03T05:03:15.237Z" }, + { url = "https://files.pythonhosted.org/packages/75/a4/9431ba598651d60ebd50dc25755402b770322cf8432adcc07d2906e53a54/pyzmq-27.0.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:696900ef6bc20bef6a242973943574f96c3f97d2183c1bd3da5eea4f559631b1", size = 908249, upload-time = "2025-08-03T05:03:16.933Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/e624e1793689e4e685d2ee21c40277dd4024d9d730af20446d88f69be838/pyzmq-27.0.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96a63aecec22d3f7fdea3c6c98df9e42973f5856bb6812c3d8d78c262fee808", size = 668649, upload-time = "2025-08-03T05:03:18.49Z" }, + { url = "https://files.pythonhosted.org/packages/6c/29/0652a39d4e876e0d61379047ecf7752685414ad2e253434348246f7a2a39/pyzmq-27.0.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c512824360ea7490390566ce00bee880e19b526b312b25cc0bc30a0fe95cb67f", size = 856601, upload-time = "2025-08-03T05:03:20.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/2d/8d5355d7fc55bb6e9c581dd74f58b64fa78c994079e3a0ea09b1b5627cde/pyzmq-27.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dfb2bb5e0f7198eaacfb6796fb0330afd28f36d985a770745fba554a5903595a", size = 1657750, upload-time = "2025-08-03T05:03:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f4/cd032352d5d252dc6f5ee272a34b59718ba3af1639a8a4ef4654f9535cf5/pyzmq-27.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4f6886c59ba93ffde09b957d3e857e7950c8fe818bd5494d9b4287bc6d5bc7f1", size = 2034312, upload-time = "2025-08-03T05:03:23.578Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1a/c050d8b6597200e97a4bd29b93c769d002fa0b03083858227e0376ad59bc/pyzmq-27.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b99ea9d330e86ce1ff7f2456b33f1bf81c43862a5590faf4ef4ed3a63504bdab", size = 1893632, upload-time = "2025-08-03T05:03:25.167Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/173ce21d5097e7fcf284a090e8beb64fc683c6582b1f00fa52b1b7e867ce/pyzmq-27.0.1-cp311-cp311-win32.whl", hash = "sha256:571f762aed89025ba8cdcbe355fea56889715ec06d0264fd8b6a3f3fa38154ed", size = 566587, upload-time = "2025-08-03T05:03:26.769Z" }, + { url = "https://files.pythonhosted.org/packages/53/ab/22bd33e7086f0a2cc03a5adabff4bde414288bb62a21a7820951ef86ec20/pyzmq-27.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee16906c8025fa464bea1e48128c048d02359fb40bebe5333103228528506530", size = 632873, upload-time = "2025-08-03T05:03:28.685Z" }, + { url = "https://files.pythonhosted.org/packages/90/14/3e59b4a28194285ceeff725eba9aa5ba8568d1cb78aed381dec1537c705a/pyzmq-27.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:ba068f28028849da725ff9185c24f832ccf9207a40f9b28ac46ab7c04994bd41", size = 558918, upload-time = "2025-08-03T05:03:30.085Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9b/c0957041067c7724b310f22c398be46399297c12ed834c3bc42200a2756f/pyzmq-27.0.1-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:af7ebce2a1e7caf30c0bb64a845f63a69e76a2fadbc1cac47178f7bb6e657bdd", size = 1305432, upload-time = "2025-08-03T05:03:32.177Z" }, + { url = "https://files.pythonhosted.org/packages/8e/55/bd3a312790858f16b7def3897a0c3eb1804e974711bf7b9dcb5f47e7f82c/pyzmq-27.0.1-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8f617f60a8b609a13099b313e7e525e67f84ef4524b6acad396d9ff153f6e4cd", size = 895095, upload-time = "2025-08-03T05:03:33.918Z" }, + { url = "https://files.pythonhosted.org/packages/20/50/fc384631d8282809fb1029a4460d2fe90fa0370a0e866a8318ed75c8d3bb/pyzmq-27.0.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d59dad4173dc2a111f03e59315c7bd6e73da1a9d20a84a25cf08325b0582b1a", size = 651826, upload-time = "2025-08-03T05:03:35.818Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0a/2356305c423a975000867de56888b79e44ec2192c690ff93c3109fd78081/pyzmq-27.0.1-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5b6133c8d313bde8bd0d123c169d22525300ff164c2189f849de495e1344577", size = 839751, upload-time = "2025-08-03T05:03:37.265Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1b/81e95ad256ca7e7ccd47f5294c1c6da6e2b64fbace65b84fe8a41470342e/pyzmq-27.0.1-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:58cca552567423f04d06a075f4b473e78ab5bdb906febe56bf4797633f54aa4e", size = 1641359, upload-time = "2025-08-03T05:03:38.799Z" }, + { url = "https://files.pythonhosted.org/packages/50/63/9f50ec965285f4e92c265c8f18344e46b12803666d8b73b65d254d441435/pyzmq-27.0.1-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:4b9d8e26fb600d0d69cc9933e20af08552e97cc868a183d38a5c0d661e40dfbb", size = 2020281, upload-time = "2025-08-03T05:03:40.338Z" }, + { url = "https://files.pythonhosted.org/packages/02/4a/19e3398d0dc66ad2b463e4afa1fc541d697d7bc090305f9dfb948d3dfa29/pyzmq-27.0.1-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2329f0c87f0466dce45bba32b63f47018dda5ca40a0085cc5c8558fea7d9fc55", size = 1877112, upload-time = "2025-08-03T05:03:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/bf/42/c562e9151aa90ed1d70aac381ea22a929d6b3a2ce4e1d6e2e135d34fd9c6/pyzmq-27.0.1-cp312-abi3-win32.whl", hash = "sha256:57bb92abdb48467b89c2d21da1ab01a07d0745e536d62afd2e30d5acbd0092eb", size = 558177, upload-time = "2025-08-03T05:03:43.979Z" }, + { url = "https://files.pythonhosted.org/packages/40/96/5c50a7d2d2b05b19994bf7336b97db254299353dd9b49b565bb71b485f03/pyzmq-27.0.1-cp312-abi3-win_amd64.whl", hash = "sha256:ff3f8757570e45da7a5bedaa140489846510014f7a9d5ee9301c61f3f1b8a686", size = 618923, upload-time = "2025-08-03T05:03:45.438Z" }, + { url = "https://files.pythonhosted.org/packages/13/33/1ec89c8f21c89d21a2eaff7def3676e21d8248d2675705e72554fb5a6f3f/pyzmq-27.0.1-cp312-abi3-win_arm64.whl", hash = "sha256:df2c55c958d3766bdb3e9d858b911288acec09a9aab15883f384fc7180df5bed", size = 552358, upload-time = "2025-08-03T05:03:46.887Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1a/49f66fe0bc2b2568dd4280f1f520ac8fafd73f8d762140e278d48aeaf7b9/pyzmq-27.0.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7fb0ee35845bef1e8c4a152d766242164e138c239e3182f558ae15cb4a891f94", size = 835949, upload-time = "2025-08-03T05:05:13.798Z" }, + { url = "https://files.pythonhosted.org/packages/49/94/443c1984b397eab59b14dd7ae8bc2ac7e8f32dbc646474453afcaa6508c4/pyzmq-27.0.1-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f379f11e138dfd56c3f24a04164f871a08281194dd9ddf656a278d7d080c8ad0", size = 799875, upload-time = "2025-08-03T05:05:15.632Z" }, + { url = "https://files.pythonhosted.org/packages/30/f1/fd96138a0f152786a2ba517e9c6a8b1b3516719e412a90bb5d8eea6b660c/pyzmq-27.0.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b978c0678cffbe8860ec9edc91200e895c29ae1ac8a7085f947f8e8864c489fb", size = 567403, upload-time = "2025-08-03T05:05:17.326Z" }, + { url = "https://files.pythonhosted.org/packages/16/57/34e53ef2b55b1428dac5aabe3a974a16c8bda3bf20549ba500e3ff6cb426/pyzmq-27.0.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ebccf0d760bc92a4a7c751aeb2fef6626144aace76ee8f5a63abeb100cae87f", size = 747032, upload-time = "2025-08-03T05:05:19.074Z" }, + { url = "https://files.pythonhosted.org/packages/81/b7/769598c5ae336fdb657946950465569cf18803140fe89ce466d7f0a57c11/pyzmq-27.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:77fed80e30fa65708546c4119840a46691290efc231f6bfb2ac2a39b52e15811", size = 544566, upload-time = "2025-08-03T05:05:20.798Z" }, ] [[package]] @@ -4562,25 +4536,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] -[[package]] -name = "rerun-sdk" -version = "0.24.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "numpy" }, - { name = "pillow" }, - { name = "pyarrow" }, - { name = "typing-extensions" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/c8/c001c855e91fbc09ba7340839070f7452cd6dfd56e40e38a1a6cc826424c/rerun_sdk-0.24.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:3d3f0e63f0a26c3f058d28d6267958d4d7bf03f63e2a16a45cb6ef4161dcb55e", size = 84079586, upload-time = "2025-07-17T15:21:39.835Z" }, - { url = "https://files.pythonhosted.org/packages/f2/df/751bda39d62304f22776b30edf8efcfb8920b4e8c6a388ee12ccd7c6e88f/rerun_sdk-0.24.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:35ee215d186bc410f4c38c7dc9f260604e857695cd9df996510326107d73c238", size = 78344845, upload-time = "2025-07-17T15:21:45.002Z" }, - { url = "https://files.pythonhosted.org/packages/30/0f/e0d37550744d76968eb5affc2aff748cf06a93e90e013fe127ede831f98d/rerun_sdk-0.24.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a0f8ac65ab0da000c9f23ef13440581050fa05f071ed23653cd10f134945479d", size = 86327955, upload-time = "2025-07-17T15:21:50.016Z" }, - { url = "https://files.pythonhosted.org/packages/25/6d/94dc5388c0223b33843c1ae7dae6627caf543faf5d49c4f7ebfbdc3e4f0e/rerun_sdk-0.24.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a4e5c7452df556fc5f64667cb3974ddcf01e3fc03374748348de6855f2919397", size = 90706767, upload-time = "2025-07-17T15:21:54.994Z" }, - { url = "https://files.pythonhosted.org/packages/00/88/f3c738ae799423fe0615fb6e6b94d2c02e28a090bea0db260e38ff1113ed/rerun_sdk-0.24.0-cp39-abi3-win_amd64.whl", hash = "sha256:4c5049f04f4b224cc342d831384b510d157bd7eff79aa267a88b546d0d12b36e", size = 72867282, upload-time = "2025-07-17T15:22:01.032Z" }, -] - [[package]] name = "ruamel-yaml" version = "0.18.14" @@ -4621,36 +4576,36 @@ wheels = [ [[package]] name = "rubicon-objc" -version = "0.5.1" +version = "0.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/83/e57741dcf862a2581d53eccf8b11749c97f73d9754bbc538fb6c7b527da3/rubicon_objc-0.5.1.tar.gz", hash = "sha256:90bee9fc1de4515e17615e15648989b88bb8d4d2ffc8c7c52748272cd7f30a66", size = 174639, upload-time = "2025-06-03T06:33:50.822Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/2f/612049b8601e810080f63f4b85acbf2ed0784349088d3c944eb288e1d487/rubicon_objc-0.5.2.tar.gz", hash = "sha256:1180593935f6a8a39c23b5f4b7baa24aedf9f7285e80804a1d9d6b50a50572f5", size = 175710, upload-time = "2025-08-07T06:12:25.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/0a/e451c3dbda38dd6abab1fd16c3b35623fc0635dffcbbf97f1acc55a58508/rubicon_objc-0.5.1-py3-none-any.whl", hash = "sha256:17092756241b8370231cfaad45ad6e8ce99534987f2acbc944d65df5bdf8f6cd", size = 63323, upload-time = "2025-06-03T06:33:48.863Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a4/11ad29100060af56408ed084dca76a16d2ce8eb31b75081bfc0eec45d755/rubicon_objc-0.5.2-py3-none-any.whl", hash = "sha256:829b253c579e51fc34f4bb6587c34806e78960dcc1eb24e62b38141a1fe02b39", size = 63512, upload-time = "2025-08-07T06:12:23.766Z" }, ] [[package]] name = "ruff" -version = "0.12.5" +version = "0.12.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/cd/01015eb5034605fd98d829c5839ec2c6b4582b479707f7c1c2af861e8258/ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e", size = 5170722, upload-time = "2025-07-24T13:26:37.456Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/de/ad2f68f0798ff15dd8c0bcc2889558970d9a685b3249565a937cd820ad34/ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92", size = 11819133, upload-time = "2025-07-24T13:25:56.369Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fc/c6b65cd0e7fbe60f17e7ad619dca796aa49fbca34bb9bea5f8faf1ec2643/ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a", size = 12501114, upload-time = "2025-07-24T13:25:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/c5/de/c6bec1dce5ead9f9e6a946ea15e8d698c35f19edc508289d70a577921b30/ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf", size = 11716873, upload-time = "2025-07-24T13:26:01.496Z" }, - { url = "https://files.pythonhosted.org/packages/a1/16/cf372d2ebe91e4eb5b82a2275c3acfa879e0566a7ac94d331ea37b765ac8/ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73", size = 11958829, upload-time = "2025-07-24T13:26:03.721Z" }, - { url = "https://files.pythonhosted.org/packages/25/bf/cd07e8f6a3a6ec746c62556b4c4b79eeb9b0328b362bb8431b7b8afd3856/ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac", size = 11626619, upload-time = "2025-07-24T13:26:06.118Z" }, - { url = "https://files.pythonhosted.org/packages/d8/c9/c2ccb3b8cbb5661ffda6925f81a13edbb786e623876141b04919d1128370/ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08", size = 13221894, upload-time = "2025-07-24T13:26:08.292Z" }, - { url = "https://files.pythonhosted.org/packages/6b/58/68a5be2c8e5590ecdad922b2bcd5583af19ba648f7648f95c51c3c1eca81/ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4", size = 14163909, upload-time = "2025-07-24T13:26:10.474Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d1/ef6b19622009ba8386fdb792c0743f709cf917b0b2f1400589cbe4739a33/ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b", size = 13583652, upload-time = "2025-07-24T13:26:13.381Z" }, - { url = "https://files.pythonhosted.org/packages/62/e3/1c98c566fe6809a0c83751d825a03727f242cdbe0d142c9e292725585521/ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a", size = 12700451, upload-time = "2025-07-24T13:26:15.488Z" }, - { url = "https://files.pythonhosted.org/packages/24/ff/96058f6506aac0fbc0d0fc0d60b0d0bd746240a0594657a2d94ad28033ba/ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a", size = 12937465, upload-time = "2025-07-24T13:26:17.808Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d3/68bc5e7ab96c94b3589d1789f2dd6dd4b27b263310019529ac9be1e8f31b/ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5", size = 11771136, upload-time = "2025-07-24T13:26:20.422Z" }, - { url = "https://files.pythonhosted.org/packages/52/75/7356af30a14584981cabfefcf6106dea98cec9a7af4acb5daaf4b114845f/ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293", size = 11601644, upload-time = "2025-07-24T13:26:22.928Z" }, - { url = "https://files.pythonhosted.org/packages/c2/67/91c71d27205871737cae11025ee2b098f512104e26ffd8656fd93d0ada0a/ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb", size = 12478068, upload-time = "2025-07-24T13:26:26.134Z" }, - { url = "https://files.pythonhosted.org/packages/34/04/b6b00383cf2f48e8e78e14eb258942fdf2a9bf0287fbf5cdd398b749193a/ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb", size = 12991537, upload-time = "2025-07-24T13:26:28.533Z" }, - { url = "https://files.pythonhosted.org/packages/3e/b9/053d6445dc7544fb6594785056d8ece61daae7214859ada4a152ad56b6e0/ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9", size = 11751575, upload-time = "2025-07-24T13:26:30.835Z" }, - { url = "https://files.pythonhosted.org/packages/bc/0f/ab16e8259493137598b9149734fec2e06fdeda9837e6f634f5c4e35916da/ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5", size = 12882273, upload-time = "2025-07-24T13:26:32.929Z" }, - { url = "https://files.pythonhosted.org/packages/00/db/c376b0661c24cf770cb8815268190668ec1330eba8374a126ceef8c72d55/ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805", size = 11951564, upload-time = "2025-07-24T13:26:34.994Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, + { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, + { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, + { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, + { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, + { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" }, + { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, + { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" }, + { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" }, ] [[package]] @@ -4664,15 +4619,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.33.2" +version = "2.34.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/82/dfe4a91fd38e048fbb55ca6c072710408e8802015aa27cde18e8684bb1e9/sentry_sdk-2.33.2.tar.gz", hash = "sha256:e85002234b7b8efac9b74c2d91dbd4f8f3970dc28da8798e39530e65cb740f94", size = 335804, upload-time = "2025-07-22T10:41:18.578Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/38/10d6bfe23df1bfc65ac2262ed10b45823f47f810b0057d3feeea1ca5c7ed/sentry_sdk-2.34.1.tar.gz", hash = "sha256:69274eb8c5c38562a544c3e9f68b5be0a43be4b697f5fd385bf98e4fbe672687", size = 336969, upload-time = "2025-07-30T11:13:37.93Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/dc/4d825d5eb6e924dfcc6a91c8185578a7b0a5c41fd2416a6f49c8226d6ef9/sentry_sdk-2.33.2-py2.py3-none-any.whl", hash = "sha256:8d57a3b4861b243aa9d558fda75509ad487db14f488cbdb6c78c614979d77632", size = 356692, upload-time = "2025-07-22T10:41:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3e/bb34de65a5787f76848a533afbb6610e01fbcdd59e76d8679c254e02255c/sentry_sdk-2.34.1-py2.py3-none-any.whl", hash = "sha256:b7a072e1cdc5abc48101d5146e1ae680fa81fe886d8d95aaa25a0b450c818d32", size = 357743, upload-time = "2025-07-30T11:13:36.145Z" }, ] [[package]] @@ -4846,14 +4801,14 @@ wheels = [ [[package]] name = "types-requests" -version = "2.32.4.20250611" +version = "2.32.4.20250809" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118, upload-time = "2025-06-11T03:11:41.272Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/b0/9355adb86ec84d057fea765e4c49cce592aaf3d5117ce5609a95a7fc3dac/types_requests-2.32.4.20250809.tar.gz", hash = "sha256:d8060de1c8ee599311f56ff58010fb4902f462a1470802cf9f6ed27bc46c4df3", size = 23027, upload-time = "2025-08-09T03:17:10.664Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643, upload-time = "2025-06-11T03:11:40.186Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6f/ec0012be842b1d888d46884ac5558fd62aeae1f0ec4f7a581433d890d4b5/types_requests-2.32.4.20250809-py3-none-any.whl", hash = "sha256:f73d1832fb519ece02c85b1f09d5f0dd3108938e7d47e7f94bbfa18a6782b163", size = 20644, upload-time = "2025-08-09T03:17:09.716Z" }, ] [[package]]