diff --git a/.codespellignore b/.codespellignore index c7beb6f04f..9328517d4a 100644 --- a/.codespellignore +++ b/.codespellignore @@ -2,3 +2,4 @@ Wen REGIST PullRequest cancelled +FOF 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 d3cfa60d92..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: @@ -29,17 +29,17 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - target: /^(?!master-new$).*/ + target: /^(?!master$).*/ exclude: /sunnypilot:.*/ change-to: ${{ github.base_ref }} already-exists-action: close_this - already-exists-comment: "Your PR should be made against the `master-new` branch" + already-exists-comment: "Your PR should be made against the `master` branch" update-pr-labels: 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 new file mode 100644 index 0000000000..1c7812ee47 --- /dev/null +++ b/.github/workflows/build-all-tinygrad-models.yaml @@ -0,0 +1,303 @@ +name: Build and push all tinygrad models + +on: + workflow_dispatch: + inputs: + set_min_version: + description: 'Minimum selector version required for the models (see helpers.py or readme.md)' + required: true + type: string + +jobs: + setup: + runs-on: ubuntu-latest + outputs: + json_version: ${{ steps.get-json.outputs.json_version }} + 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: + repository: sunnypilot/sunnypilot-docs + ref: gh-pages + path: docs + ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} + + - name: Get next JSON version to use (from GitHub docs repo) + id: get-json + run: | + cd docs/docs + latest=$(ls driving_models_v*.json | sed -E 's/.*_v([0-9]+)\.json/\1/' | sort -n | tail -1) + next=$((latest+1)) + json_file="driving_models_v${next}.json" + cp "driving_models_v${latest}.json" "$json_file" + echo "json_file=docs/docs/$json_file" >> $GITHUB_OUTPUT + echo "json_version=$((next+0))" >> $GITHUB_OUTPUT + echo "SRC_JSON_FILE=docs/docs/driving_models_v${latest}.json" >> $GITHUB_ENV + + - name: Extract tinygrad models + id: set-matrix + working-directory: docs/docs + run: | + jq -c '[.bundles[] | select(.runner=="tinygrad") | {ref, display_name: (.display_name | gsub(" \\([^)]*\\)"; "")), is_20hz}]' "$(basename "${SRC_JSON_FILE}")" > matrix.json + echo "model_matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT + + - name: Set up SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} + - run: | + mkdir -p ~/.ssh + ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts + + - name: Clone GitLab docs repo and create new recompiled dir + id: create-recompiled-dir + env: + GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts' + run: | + git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs + cd gitlab_docs + git checkout main + git sparse-checkout set --no-cone models/ + cd models + latest_dir=$(ls -d recompiled* 2>/dev/null | sed -E 's/recompiled([0-9]+)/\1/' | sort -n | tail -1) + if [[ -z "$latest_dir" ]]; then + next_dir=1 + else + next_dir=$((latest_dir+1)) + fi + recompiled_dir="${next_dir}" + mkdir -p "recompiled${recompiled_dir}" + touch "recompiled${recompiled_dir}/.gitkeep" + cd ../.. + echo "recompiled_dir=$recompiled_dir" >> $GITHUB_OUTPUT + + - name: Push empty recompiled dir to GitLab + run: | + cd gitlab_docs + git add models/recompiled${{ steps.create-recompiled-dir.outputs.recompiled_dir }} + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git commit -m "Add recompiled${{ steps.create-recompiled-dir.outputs.recompiled_dir }} for build-all" || echo "No changes to commit" + git push origin main + + - name: Push new JSON to GitHub docs repo + run: | + cd docs + git pull origin gh-pages + git add docs/"$(basename ${{ steps.get-json.outputs.json_file }})" + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git commit -m "Add new ${{ steps.get-json.outputs.json_file }} for build-all" || echo "No changes to commit" + git push origin gh-pages + + get_and_build: + needs: [setup] + strategy: + matrix: + model: ${{ fromJson(needs.setup.outputs.model_matrix) }} + fail-fast: false + uses: ./.github/workflows/build-single-tinygrad-model.yaml + with: + upstream_branch: ${{ matrix.model.ref }} + custom_name: ${{ matrix.model.display_name }} + recompiled_dir: ${{ needs.setup.outputs.recompiled_dir }} + json_version: ${{ needs.setup.outputs.json_version }} + secrets: inherit + + retry_failed_models: + needs: [setup, get_and_build] + runs-on: ubuntu-latest + if: ${{ needs.setup.result != 'failure' && !cancelled() }} + outputs: + retry_matrix: ${{ steps.set-retry-matrix.outputs.retry_matrix }} + steps: + - uses: actions/download-artifact@v4 + with: + pattern: model-* + path: output + + - id: set-retry-matrix + run: | + echo '${{ needs.setup.outputs.model_matrix }}' > matrix.json + built=(); while IFS= read -r line; do built+=("$line"); done < <( + ls output | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}' + ) + jq -c --argjson built "$(printf '%s\n' "${built[@]}" | jq -R . | jq -s .)" \ + 'map(select(.display_name as $n | ($built | index($n | gsub("^ +| +$"; "")) | not)))' matrix.json > retry_matrix.json + echo "retry_matrix=$(cat retry_matrix.json)" >> $GITHUB_OUTPUT + + retry_get_and_build: + needs: [setup, get_and_build, retry_failed_models] + if: ${{ needs.get_and_build.result == 'failure' || (needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '') }} + strategy: + matrix: + model: ${{ fromJson(needs.retry_failed_models.outputs.retry_matrix) }} + fail-fast: false + uses: ./.github/workflows/build-single-tinygrad-model.yaml + with: + upstream_branch: ${{ matrix.model.ref }} + custom_name: ${{ matrix.model.display_name }} + recompiled_dir: ${{ needs.setup.outputs.recompiled_dir }} + json_version: ${{ needs.setup.outputs.json_version }} + artifact_suffix: -retry + secrets: inherit + + publish_models: + name: Publish models sequentially + needs: [setup, get_and_build, retry_failed_models, retry_get_and_build] + if: ${{ !cancelled() && (needs.get_and_build.result != 'failure' || needs.retry_get_and_build.result == 'success' || (needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '')) }} + runs-on: ubuntu-latest + strategy: + max-parallel: 1 + matrix: + model: ${{ fromJson(needs.setup.outputs.model_matrix) }} + env: + RECOMPILED_DIR: recompiled${{ needs.setup.outputs.recompiled_dir }} + JSON_FILE: ${{ needs.setup.outputs.json_file }} + ARTIFACT_NAME_INPUT: ${{ matrix.model.display_name }} + steps: + - name: Set up SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} + + - name: Add GitLab.com SSH key to known_hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts + + - name: Clone GitLab docs repo + env: + GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts' + run: | + echo "Cloning GitLab" + git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs + cd gitlab_docs + echo "checkout models/${RECOMPILED_DIR}" + git sparse-checkout set --no-cone models/${RECOMPILED_DIR} + git checkout main + cd .. + + - name: Checkout docs repo + uses: actions/checkout@v4 + with: + repository: sunnypilot/sunnypilot-docs + ref: gh-pages + path: docs + ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} + + - name: Validate recompiled dir and JSON version + run: | + if [ ! -d "gitlab_docs/models/$RECOMPILED_DIR" ]; then + echo "Recompiled dir $RECOMPILED_DIR does not exist in GitLab repo" + exit 1 + fi + if [ ! -f "$JSON_FILE" ]; then + echo "JSON file $JSON_FILE does not exist!" + exit 1 + fi + + - name: Download artifact name file + uses: actions/download-artifact@v4 + with: + name: artifact-name-${{ env.ARTIFACT_NAME_INPUT }} + path: artifact_name + + - name: Read artifact name + id: read-artifact-name + run: | + ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt) + echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT + + - name: Download model artifact + uses: actions/download-artifact@v4 + with: + name: ${{ steps.read-artifact-name.outputs.artifact_name }} + path: output + + - name: Remove onnx files bc not needed for recompiled dir since they already exist from single build + run: | + find output -type f -name '*.onnx' -delete + find output -type f -name 'big_*.pkl' -delete + find output -type f -name 'dmonitoring_model_tinygrad.pkl' -delete + + - name: Copy model artifacts to gitlab + env: + ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }} + run: | + ARTIFACT_DIR="gitlab_docs/models/${RECOMPILED_DIR}/${ARTIFACT_NAME}" + mkdir -p "$ARTIFACT_DIR" + for path in output/*; do + if [ "$(basename "$path")" = "artifact_name.txt" ]; then + continue + fi + name="$(basename "$path")" + if [ -d "$path" ]; then + mkdir -p "$ARTIFACT_DIR/$name" + cp -r "$path"/* "$ARTIFACT_DIR/$name/" + echo "Copied dir $name -> $ARTIFACT_DIR/$name" + else + cp "$path" "$ARTIFACT_DIR/" + echo "Copied file $name -> $ARTIFACT_DIR/" + fi + done + + - name: Push recompiled dir to GitLab + env: + GITLAB_SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} + run: | + cd gitlab_docs + git checkout main + git pull origin main + for d in models/"$RECOMPILED_DIR"/*/; do + git sparse-checkout add "$d" + done + git add models/"$RECOMPILED_DIR" + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git commit -m "Update $RECOMPILED_DIR with model from build-all-tinygrad-models" || echo "No changes to commit" + git push origin main + - run: | + cd docs + git pull origin gh-pages + + - name: update json + run: | + 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" \ + $ARGS + + - name: Push updated json to GitHub + run: | + cd docs + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git checkout gh-pages + git add docs/"$(basename $JSON_FILE)" + git commit -m "Update $(basename $JSON_FILE) after recompiling model" || echo "No changes to commit" + git push origin gh-pages diff --git a/.github/workflows/build-single-tinygrad-model.yaml b/.github/workflows/build-single-tinygrad-model.yaml new file mode 100644 index 0000000000..76f39fb6fe --- /dev/null +++ b/.github/workflows/build-single-tinygrad-model.yaml @@ -0,0 +1,228 @@ +name: Build Single Tinygrad Model and Push + +on: + workflow_call: + inputs: + upstream_branch: + description: 'Upstream commit to build from' + required: true + type: string + custom_name: + description: 'Custom name for the model (no date, only name)' + required: false + type: string + recompiled_dir: + description: 'Existing recompiled directory number (e.g. 3 for recompiled3)' + required: true + type: string + json_version: + description: 'driving_models version number to update (e.g. 5 for driving_models_v5.json)' + required: true + type: string + artifact_suffix: + description: 'Suffix for artifact name' + required: false + type: string + default: '' + bypass_push: + description: 'Bypass pushing to GitLab for build-all' + required: false + default: true + type: boolean + workflow_dispatch: + inputs: + upstream_branch: + description: 'Upstream commit to build from' + required: true + type: string + custom_name: + description: 'Custom name for the model (no date, only name)' + required: false + type: string + recompiled_dir: + description: 'Existing recompiled directory number (e.g. 3 for recompiled3)' + required: true + type: string + json_version: + description: 'driving_models version number to update (e.g. 5 for driving_models_v5.json)' + required: true + type: string + model_folder: + description: 'Model folder' + type: choice + default: 'None' + options: + - None + - Simple Plan Models + - Space Lab Models + - TR Models + - DTR Models + - Custom Merge Models + - FOF series models + - Other + custom_model_folder: + description: 'Custom model folder name (if "Other" selected)' + required: false + type: string + generation: + description: 'Model generation' + required: false + type: string + version: + description: 'Minimum selector version' + required: false + type: string +env: + RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }} + JSON_FILE: docs/docs/driving_models_v${{ inputs.json_version }}.json + +jobs: + build_model: + uses: ./.github/workflows/sunnypilot-build-model.yaml + with: + upstream_branch: ${{ inputs.upstream_branch }} + custom_name: ${{ inputs.custom_name || inputs.upstream_branch }} + is_20hz: true + artifact_suffix: ${{ inputs.artifact_suffix }} + secrets: inherit + + publish_model: + if: ${{ !inputs.bypass_push && !cancelled() }} + concurrency: + group: gitlab-push-${{ inputs.recompiled_dir }} + cancel-in-progress: false + needs: build_model + runs-on: ubuntu-latest + steps: + - name: Set up SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} + + - name: Add GitLab.com SSH key to known_hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts + + - name: Clone GitLab docs repo + env: + GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts' + run: | + echo "Cloning GitLab" + git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs + cd gitlab_docs + echo "checkout models/${RECOMPILED_DIR}" + git sparse-checkout set --no-cone models/${RECOMPILED_DIR} + git checkout main + cd .. + + - name: Checkout docs repo + uses: actions/checkout@v4 + with: + repository: sunnypilot/sunnypilot-docs + ref: gh-pages + path: docs + ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} + + - name: Validate recompiled dir and JSON version + run: | + if [ ! -d "gitlab_docs/models/$RECOMPILED_DIR" ]; then + echo "Recompiled dir $RECOMPILED_DIR does not exist in GitLab repo" + exit 1 + fi + if [ ! -f "$JSON_FILE" ]; then + echo "JSON file $JSON_FILE does not exist!" + exit 1 + fi + + - name: Download artifact name file + uses: actions/download-artifact@v4 + with: + name: artifact-name-${{ inputs.custom_name || inputs.upstream_branch }} + path: artifact_name + + - name: Read artifact name + id: read-artifact-name + run: | + ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt) + echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT + + - name: Download and extract model artifact + uses: actions/download-artifact@v4 + with: + name: ${{ steps.read-artifact-name.outputs.artifact_name }} + path: output + + - name: Remove unwanted files + run: | + find output -type f -name 'dmonitoring_model_tinygrad.pkl' -delete + find output -type f -name 'dmonitoring_model.onnx' -delete + + - name: Copy model artifact(s) to GitLab recompiled dir + env: + ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }} + run: | + ARTIFACT_DIR="gitlab_docs/models/${RECOMPILED_DIR}/${ARTIFACT_NAME}" + mkdir -p "$ARTIFACT_DIR" + for path in output/*; do + if [ "$(basename "$path")" = "artifact_name.txt" ]; then + continue + fi + name="$(basename "$path")" + if [ -d "$path" ]; then + mkdir -p "$ARTIFACT_DIR/$name" + cp -r "$path"/* "$ARTIFACT_DIR/$name/" + echo "Copied dir $name -> $ARTIFACT_DIR/$name" + else + cp "$path" "$ARTIFACT_DIR/" + echo "Copied file $name -> $ARTIFACT_DIR/" + fi + done + + - name: Push recompiled dir to GitLab + env: + GITLAB_SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} + run: | + cd gitlab_docs + git checkout main + git pull origin main + for d in models/"$RECOMPILED_DIR"/*/; do + git sparse-checkout add "$d" + done + git add models/"$RECOMPILED_DIR" + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git commit -m "Create/Update $RECOMPILED_DIR with new/updated model from build-single-tinygrad-model" || echo "No changes to commit" + git push origin main + + - run: | + cd docs + git pull origin gh-pages + + - name: Run json_parser.py to update JSON + run: | + FOLDER="${{ inputs.model_folder }}" + if [ "$FOLDER" = "Other" ]; then + FOLDER="${{ inputs.custom_model_folder }}" + fi + ARGS="" + if [ "$FOLDER" != "None" ] && [ -n "$FOLDER" ]; then + ARGS="$ARGS --model-folder \"$FOLDER\"" + fi + [ -n "${{ inputs.generation }}" ] && ARGS="$ARGS --generation \"${{ inputs.generation }}\"" + [ -n "${{ inputs.version }}" ] && ARGS="$ARGS --version \"${{ inputs.version }}\"" + eval python3 docs/json_parser.py \ + --json-path "$JSON_FILE" \ + --recompiled-dir "gitlab_docs/models/$RECOMPILED_DIR" \ + --sort-by-date \ + $ARGS + + - name: Push updated JSON to GitHub docs repo + run: | + cd docs + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git checkout gh-pages + git add docs/"$(basename $JSON_FILE)" + git commit -m "Update $(basename $JSON_FILE) after recompiling model" || echo "No changes to commit" + git push origin gh-pages diff --git a/.github/workflows/cereal_validation.yaml b/.github/workflows/cereal_validation.yaml index 5d75b6bc13..2290481c56 100644 --- a/.github/workflows/cereal_validation.yaml +++ b/.github/workflows/cereal_validation.yaml @@ -4,7 +4,6 @@ on: push: branches: - master - - master-new pull_request: paths: - 'cereal/**' @@ -17,7 +16,7 @@ on: type: string concurrency: - group: cereal-validation-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} + group: cereal-validation-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} cancel-in-progress: true env: diff --git a/.github/workflows/compile-openpilot/action.yaml b/.github/workflows/compile-openpilot/action.yaml index 87590b41cb..4015746c0e 100644 --- a/.github/workflows/compile-openpilot/action.yaml +++ b/.github/workflows/compile-openpilot/action.yaml @@ -15,7 +15,7 @@ runs: scons -j$(nproc) --cache-populate" - name: Save scons cache uses: actions/cache/save@v4 - if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') + if: github.ref == 'refs/heads/master' with: path: .ci_cache/scons_cache key: scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} 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 954dc9128d..8780abfbb3 100644 --- a/.github/workflows/lfs-maintenance.yaml +++ b/.github/workflows/lfs-maintenance.yaml @@ -9,11 +9,11 @@ on: - cron: '0 0 * * *' # Runs at 00:00 UTC every day push: branches: - - 'master-new' + - 'master' pull_request: branches: - - 'master-new' - workflow_dispatch: # enables manual triggering + - 'master' + 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-drafter.yml b/.github/workflows/release-drafter.yml index 62712a770c..c072e98e24 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -3,7 +3,6 @@ name: Release Drafter on: push: branches: - - master-new - master tags: - 'v*' 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 6bd86ce7e9..72de377086 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -6,14 +6,14 @@ 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: update_translations: runs-on: ubuntu-latest - if: github.repository == 'commaai/openpilot' + if: github.repository == 'sunnypilot/sunnypilot' steps: - uses: actions/checkout@v4 - uses: ./.github/workflows/setup-with-retry @@ -23,7 +23,7 @@ jobs: - name: Create Pull Request uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83 with: - author: Vehicle Researcher + author: github-actions[bot] commit-message: "Update translations" title: "[bot] Update translations" body: "Automatic PR from repo-maintenance -> update_translations" @@ -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 @@ -61,8 +61,8 @@ jobs: - name: Create Pull Request uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83 with: - author: Vehicle Researcher - token: ${{ secrets.ACTIONS_CREATE_PR_PAT }} + author: github-actions[bot] + token: ${{ github.repository == 'commaai/openpilot' && secrets.ACTIONS_CREATE_PR_PAT || secrets.GITHUB_TOKEN }} commit-message: Update Python packages title: '[bot] Update Python packages' branch: auto-package-updates diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index f8897258c6..912ac787c3 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -4,7 +4,6 @@ on: push: branches: - master - - master-new pull_request: workflow_dispatch: workflow_call: @@ -15,28 +14,30 @@ on: type: string concurrency: - group: selfdrive-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} + group: selfdrive-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} cancel-in-progress: true env: - REPORT_NAME: report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && '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 - PYTEST: pytest --continue-on-collection-errors --cov --cov-report=xml --cov-append --durations=0 --durations-min=5 --hypothesis-seed 0 -n logical + PYTEST: pytest --continue-on-collection-errors --durations=0 --durations-min=5 -n logical 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: @@ -66,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 @@ -86,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 @@ -107,7 +129,7 @@ jobs: PYTHONWARNINGS: default - name: Save Homebrew cache uses: actions/cache/save@v4 - if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') + if: github.ref == 'refs/heads/master' with: path: ~/Library/Caches/Homebrew key: brew-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} @@ -124,15 +146,19 @@ jobs: run: . .venv/bin/activate && scons -j$(nproc) - name: Save scons cache uses: actions/cache/save@v4 - if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') + if: github.ref == 'refs/heads/master' with: path: /tmp/scons_cache key: scons-${{ runner.arch }}-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} 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: @@ -146,40 +172,45 @@ 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 && \ chmod -R 777 /tmp/comma_download_cache" - - name: "Upload coverage to Codecov" - uses: codecov/codecov-action@v4 - with: - name: ${{ github.job }} - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 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 @@ -190,12 +221,10 @@ 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 }} "coverage run selfdrive/test/process_replay/test_processes.py -j$(nproc) && \ - chmod -R 777 /tmp/comma_download_cache && \ - coverage combine && \ - coverage xml" + ${{ env.RUN }} "selfdrive/test/process_replay/test_processes.py -j$(nproc) && \ + chmod -R 777 /tmp/comma_download_cache" - name: Print diff id: print-diff if: always() @@ -207,7 +236,7 @@ jobs: name: process_replay_diff.txt path: selfdrive/test/process_replay/diff.txt - name: Upload reference logs - if: ${{ failure() && steps.print-diff.outcome == 'success' && github.repository == 'commaai/openpilot' && env.AZURE_TOKEN != '' }} + if: false # TODO: move this to github instead of azure run: | ${{ env.RUN }} "unset PYTHONWARNINGS && AZURE_TOKEN='$AZURE_TOKEN' python3 selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only" - name: Run regen @@ -216,119 +245,27 @@ jobs: run: | ${{ env.RUN }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \ chmod -R 777 /tmp/comma_download_cache" - - name: "Upload coverage to Codecov" - uses: codecov/codecov-action@v4 - with: - name: ${{ github.job }} - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - 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 }} - - name: "Upload coverage to Codecov" - uses: codecov/codecov-action@v4 - with: - name: ${{ github.job }}-${{ matrix.job }} - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - 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 && \ @@ -337,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: @@ -362,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/stale.yaml b/.github/workflows/stale.yaml index cc29813952..075b617245 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -7,6 +7,7 @@ on: env: DAYS_BEFORE_PR_CLOSE: 2 DAYS_BEFORE_PR_STALE: 9 + DAYS_BEFORE_PR_STALE_DRAFT: 30 jobs: stale: @@ -24,6 +25,28 @@ jobs: exempt-pr-labels: "ignore stale,needs testing" # if wip or it needs testing from the community, don't mark as stale days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }} days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }} + exempt-draft-pr: false + + # issue config + days-before-issue-stale: -1 # ignore issues for now + + # same as above, but give draft PRs more time + stale_drafts: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + exempt-all-milestones: true + + # pull request config + stale-pr-message: 'This PR has had no activity for ${{ env.DAYS_BEFORE_PR_STALE_DRAFT }} days. It will be automatically closed in ${{ env.DAYS_BEFORE_PR_CLOSE }} days if there is no activity.' + close-pr-message: 'This PR has been automatically closed due to inactivity. Feel free to re-open once activity resumes.' + stale-pr-label: stale + delete-branch: ${{ github.event.pull_request.head.repo.full_name == 'commaai/openpilot' }} # only delete branches on the main repo + exempt-pr-labels: "ignore stale,needs testing" # if wip or it needs testing from the community, don't mark as stale + days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE_DRAFT }} + days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }} + exempt-draft-pr: true # issue config days-before-issue-stale: -1 # ignore issues for now diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index dc02d1d8d2..dfe84e7853 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -9,6 +9,27 @@ env: MODELS_DIR: ${{ github.workspace }}/selfdrive/modeld/models on: + workflow_call: + inputs: + upstream_branch: + description: 'Upstream branch to build from' + required: true + default: 'master' + type: string + custom_name: + description: 'Custom name for the model (no date, only name)' + required: false + type: string + is_20hz: + description: 'Is this a 20Hz model' + required: false + type: boolean + default: true + artifact_suffix: + description: 'Suffix for artifact name' + required: false + type: string + default: '' workflow_dispatch: inputs: upstream_branch: @@ -32,34 +53,53 @@ run-name: Build model [${{ inputs.custom_name || inputs.upstream_branch }}] from jobs: get_model: runs-on: ubuntu-latest + env: + REF: ${{ inputs.upstream_branch }} outputs: model_date: ${{ steps.commit-date.outputs.model_date }} steps: - - uses: actions/checkout@v4 + # Note: To allow dynamic models from both openpilot and sunnypilot (merges/mashups), we try commaai as default, + # and fallback to sunnypilot if the ref checkout fails. + - name: Checkout commaai/openpilot + id: checkout_upstream + continue-on-error: true + uses: actions/checkout@v4 with: - repository: ${{ env.UPSTREAM_REPO }} - ref: ${{ github.event.inputs.upstream_branch }} + repository: commaai/openpilot + ref: ${{ inputs.upstream_branch }} submodules: recursive + path: openpilot + + - name: Fallback to sunnypilot/sunnypilot + if: steps.checkout_upstream.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: sunnypilot/sunnypilot + ref: ${{ inputs.upstream_branch }} + submodules: recursive + path: openpilot - name: Get commit date id: commit-date run: | - # Get the commit date in YYYY-MM-DD format + cd ${{ github.workspace }}/openpilot commit_date=$(git log -1 --format=%cd --date=format:'%B %d, %Y') echo "model_date=${commit_date}" >> $GITHUB_OUTPUT cat $GITHUB_OUTPUT - - run: git lfs pull + - run: | + cd ${{ github.workspace }}/openpilot + git lfs pull - name: 'Upload Artifact' uses: actions/upload-artifact@v4 with: - name: models - path: ${{ github.workspace }}/selfdrive/modeld/models/*.onnx + name: models-${{ env.REF }}${{ inputs.artifact_suffix }} + path: ${{ github.workspace }}/openpilot/selfdrive/modeld/models/*.onnx build_model: - runs-on: self-hosted + runs-on: [self-hosted, tici] needs: get_model env: MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }}) - + REF: ${{ inputs.upstream_branch }} steps: - uses: actions/checkout@v4 with: @@ -71,7 +111,7 @@ jobs: with: path: ${{env.SCONS_CACHE_DIR}} key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model-${{ github.sha }} - # Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.) + # Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.) # for security. Only caches from the default branch are shared across all builds. This is by design and cannot be overridden. restore-keys: | scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model @@ -114,7 +154,7 @@ jobs: - name: Download model artifacts uses: actions/download-artifact@v4 with: - name: models + name: models-${{ env.REF }}${{ inputs.artifact_suffix }} path: ${{ github.workspace }}/selfdrive/modeld/models - name: Build Model @@ -157,12 +197,22 @@ jobs: --upstream-branch "${{ inputs.upstream_branch }}" \ ${{ inputs.is_20hz && '--is-20hz' || '' }} + - name: Write artifact name to file + run: echo "model-${{ env.MODEL_NAME }}${{ inputs.artifact_suffix }}-${{ github.run_number }}" > ${{ env.OUTPUT_DIR }}/artifact_name.txt + - name: Upload Build Artifacts + id: upload-artifact uses: actions/upload-artifact@v4 with: - name: model-${{ env.MODEL_NAME }}-${{ github.run_number }} + name: model-${{ env.MODEL_NAME }}${{ inputs.artifact_suffix }}-${{ github.run_number }} path: ${{ env.OUTPUT_DIR }} + - name: Upload artifact name file + uses: actions/upload-artifact@v4 + with: + name: artifact-name-${{ inputs.custom_name || inputs.upstream_branch }} + path: ${{ env.OUTPUT_DIR }}/artifact_name.txt + - name: Re-enable powersave if: always() run: | diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 6cd5023a17..4f52e7fa29 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -8,7 +8,7 @@ env: PUBLIC_REPO_URL: "https://github.com/sunnypilot/sunnypilot" # Branch configurations - STAGING_C3_SOURCE_BRANCH: ${{ vars.STAGING_C3_SOURCE_BRANCH || 'master-new' }} # vars are set on repo settings. + STAGING_C3_SOURCE_BRANCH: ${{ vars.STAGING_C3_SOURCE_BRANCH || 'master' }} # vars are set on repo settings. DEV_C3_SOURCE_BRANCH: ${{ vars.DEV_C3_SOURCE_BRANCH || 'master-dev-c3-new' }} # vars are set on repo settings. # Target branch configurations @@ -21,7 +21,7 @@ env: on: push: - branches: [ master, master-new, master-dev-c3-new ] + branches: [ master, master-dev-c3-new ] tags: [ '*' ] pull_request_target: types: [ labeled ] @@ -50,7 +50,7 @@ jobs: concurrency: group: build-${{ github.head_ref || github.ref_name }} cancel-in-progress: false - runs-on: self-hosted + runs-on: [self-hosted, tici] outputs: new_branch: ${{ steps.set-env.outputs.new_branch }} version: ${{ steps.set-env.outputs.version }} @@ -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 031be6a50d..d4c201824e 100644 --- a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml @@ -1,7 +1,7 @@ name: Build dev-c3-new env: - DEFAULT_SOURCE_BRANCH: "master-new" + DEFAULT_SOURCE_BRANCH: "master" DEFAULT_TARGET_BRANCH: "master-dev-c3-new" PR_LABEL: "dev-c3" LFS_URL: 'https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs' @@ -11,18 +11,16 @@ on: push: branches: - master - - master-new pull_request_target: - types: [ labeled ] - branches: - - 'master' - - 'master-new' + types: [ labeled ] + branches: + - 'master' workflow_dispatch: inputs: source_branch: description: 'Source branch to reset from' required: true - default: 'master-new' + default: 'master' type: string target_branch: description: 'Target branch to reset and squash into' @@ -43,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: @@ -56,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 }} @@ -153,6 +151,7 @@ jobs: } }' -F label="is:pr is:open label:${PR_LABEL} draft:false sort:created-asc") + PR_LIST=${PR_LIST//\'/} echo "PR_LIST=${PR_LIST}" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index f9b24e0f2c..334b289632 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -3,20 +3,18 @@ on: push: branches: - master - - master-new pull_request_target: types: [assigned, opened, synchronize, reopened, edited] branches: - 'master' - - 'master-new' paths: - 'selfdrive/ui/**' workflow_dispatch: env: UI_JOB_NAME: "Create UI Report" - REPORT_NAME: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && 'master' || github.event.number }} - SHA: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && github.sha || github.event.pull_request.head.sha }} + REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} + SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }} BRANCH_NAME: "openpilot/pr-${{ github.event.number }}" jobs: @@ -66,7 +64,7 @@ jobs: ref: openpilot_master_ui - name: Saving new master ui - if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && github.event_name == 'push' + if: github.ref == 'refs/heads/master' && github.event_name == 'push' working-directory: ${{ github.workspace }}/master_ui run: | git checkout --orphan=new_master_ui diff --git a/.gitignore b/.gitignore index b556885632..3c0dad521d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,11 @@ venv/ model2.png a.out .hypothesis +.cache/ /docs_site/ +*.mp4 *.dylib *.DSYM *.d @@ -70,8 +72,6 @@ flycheck_* cppcheck_report.txt comma*.sh -selfdrive/modeld/thneed/compile -selfdrive/modeld/models/*.thneed selfdrive/modeld/models/*.pkl sunnypilot/modeld*/thneed/compile sunnypilot/modeld*/models/*.thneed diff --git a/.idea/tools/External Tools.xml b/.idea/tools/External Tools.xml index fa2375e2ee..d5d03136ea 100644 --- a/.idea/tools/External Tools.xml +++ b/.idea/tools/External Tools.xml @@ -2,7 +2,7 @@ @@ -16,7 +16,7 @@ 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/README.md b/README.md index 897852b03f..805c4b73d8 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Please refer to [Recommended Branches](#-recommended-branches) to find your pref ## 🎆 Pull Requests We welcome both pull requests and issues on GitHub. Bug fixes are encouraged. -Pull requests should be against the most current `master-new` branch. +Pull requests should be against the most current `master` branch. ## 📊 User Data @@ -73,7 +73,7 @@ By default, sunnypilot uploads the driving data to comma servers. You can also a sunnypilot is open source software. The user is free to disable data collection if they wish to do so. sunnypilot logs the road-facing camera, CAN, GPS, IMU, magnetometer, thermal sensors, crashes, and operating system logs. -The driver-facing camera is only logged if you explicitly opt-in in settings. The microphone is not recorded. +The driver-facing camera and microphone are only logged if you explicitly opt-in in settings. By using this software, you understand that use of this software or its related services will generate certain types of user data, which may be logged and stored at the sole discretion of comma. By accepting this agreement, you grant an irrevocable, perpetual, worldwide right to comma for the use of this data. diff --git a/RELEASES.md b/RELEASES.md index 91652d6dc7..1e80753451 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,6 +1,19 @@ -Version 0.9.10 (2025-06-30) +Version 0.10.1 (2025-09-08) ======================== +Version 0.10.0 (2025-08-05) +======================== +* New driving model + * 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) ======================== * New driving model diff --git a/SConstruct b/SConstruct index 77c8525896..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', @@ -39,10 +39,6 @@ AddOption('--clazy', action='store_true', help='build with clazy') -AddOption('--compile_db', - action='store_true', - help='build clang compilation database') - AddOption('--ccflags', action='store', type='string', @@ -86,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, @@ -94,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 = [ @@ -137,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 = [ @@ -234,8 +228,7 @@ if arch == "Darwin": darwin_rpath_link_flags = [f"-Wl,-rpath,{path}" for path in env["RPATH"]] env["LINKFLAGS"] += darwin_rpath_link_flags -if GetOption('compile_db'): - env.CompilationDatabase('compile_commands.json') +env.CompilationDatabase('compile_commands.json') # Setup cache dir default_cache_dir = '/data/scons_cache' if AGNOS else '/tmp/scons_cache' diff --git a/cereal/custom.capnp b/cereal/custom.capnp index c9e4f3e16d..fdb89da84c 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -25,6 +25,27 @@ struct ModularAssistiveDrivingSystem { } } +# Same struct as Log.RadarState.LeadData +struct LeadData { + dRel @0 :Float32; + yRel @1 :Float32; + vRel @2 :Float32; + aRel @3 :Float32; + vLead @4 :Float32; + dPath @6 :Float32; + vLat @7 :Float32; + vLeadK @8 :Float32; + aLeadK @9 :Float32; + fcw @10 :Bool; + status @11 :Bool; + aLeadTau @12 :Float32; + modelProb @13 :Float32; + radar @14 :Bool; + radarTrackId @15 :Int32 = -1; + + aLeadDEPRECATED @5 :Float32; +} + struct SelfdriveStateSP @0x81c2f05a394cf4af { mads @0 :ModularAssistiveDrivingSystem; } @@ -174,6 +195,8 @@ struct CarParamsSP @0x80ae746ee2596b11 { struct CarControlSP @0xa5cd762cd951a455 { mads @0 :ModularAssistiveDrivingSystem; params @1 :List(Param); + leadOne @2 :LeadData; + leadTwo @3 :LeadData; struct Param { key @0 :Text; diff --git a/cereal/log.capnp b/cereal/log.capnp index 7c7983d147..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; @@ -1083,7 +1085,7 @@ struct ModelDataV2 { confidence @23: ConfidenceClass; # Model perceived motion - temporalPose @21 :Pose; + temporalPoseDEPRECATED @21 :Pose; # e2e lateral planner action @26: Action; @@ -2467,16 +2469,27 @@ struct DebugAlert { alertText2 @1 :Text; } -struct UserFlag { +struct UserBookmark @0xfe346a9de48d9b50 { } -struct Microphone { +struct SoundPressure @0xdc24138990726023 { soundPressure @0 :Float32; # uncalibrated, A-weighted soundPressureWeighted @3 :Float32; soundPressureWeightedDb @1 :Float32; - filteredSoundPressureWeightedDb @2 :Float32; + + filteredSoundPressureWeightedDbDEPRECATED @2 :Float32; +} + +struct AudioData { + data @0 :Data; + sampleRate @1 :UInt32; +} + +struct AudioFeedback { + audio @0 :AudioData; + blockNum @1 :UInt16; } struct Touch { @@ -2556,7 +2569,8 @@ struct Event { livestreamDriverEncodeIdx @119 :EncodeIndex; # microphone data - microphone @103 :Microphone; + soundPressure @103 :SoundPressure; + rawAudioData @147 :AudioData; # systems stuff androidLog @20 :AndroidLogEntry; @@ -2578,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 3e72041b72..373e865e34 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -72,8 +72,11 @@ _services: dict[str, tuple] = { "navRoute": (True, 0.), "navThumbnail": (True, 0.), "qRoadEncodeIdx": (False, 20.), - "userFlag": (True, 0., 1), - "microphone": (True, 10., 10), + "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/codecov.yml b/codecov.yml deleted file mode 100644 index 519e7d1a38..0000000000 --- a/codecov.yml +++ /dev/null @@ -1,13 +0,0 @@ -comment: false -coverage: - status: - project: - default: - informational: true - patch: off - -ignore: - - "**/test_*.py" - - "selfdrive/test/**" - - "system/version.py" # codecov changes depending on if we are in a branch or not - - "tools" 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 596819256a..a60b33c4cf 100644 --- a/common/model.h +++ b/common/model.h @@ -1 +1 @@ -#define DEFAULT_MODEL "Vegetarian Filet o Fish (Default)" +#define DEFAULT_MODEL "Down To Ride (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 f0d2f6b492..ca0ef9674e 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -3,200 +3,217 @@ #include #include -inline static std::unordered_map keys = { - {"AccessToken", CLEAR_ON_MANAGER_START | DONT_LOG}, - {"AdbEnabled", PERSISTENT}, - {"AlwaysOnDM", PERSISTENT}, - {"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}, - {"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}, - {"RecordFront", PERSISTENT | BACKUP}, - {"RecordFrontLock", PERSISTENT}, // for the internal fleet - {"SecOCKey", PERSISTENT | DONT_LOG}, // Candidate for | 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}, - {"AutoLaneChangeTimer", PERSISTENT}, - {"BlinkerMinLateralControlSpeed", PERSISTENT | BACKUP}, - {"BlinkerPauseLateralControl", PERSISTENT | BACKUP}, - {"CarParamsSP", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"CarParamsSPCache", CLEAR_ON_MANAGER_START}, - {"CarParamsSPPersistent", PERSISTENT}, - {"CarPlatformBundle", PERSISTENT}, - {"CustomAccIncrementsEnabled", PERSISTENT | BACKUP}, - {"CustomAccLongPressIncrement", PERSISTENT | BACKUP}, - {"CustomAccShortPressIncrement", PERSISTENT | BACKUP}, - {"DeviceBootMode", PERSISTENT | BACKUP}, - {"EnableGithubRunner", PERSISTENT | BACKUP}, - {"MaxTimeOffroad", PERSISTENT | BACKUP}, - {"Brightness", PERSISTENT | BACKUP}, - {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, - {"OffroadMode", CLEAR_ON_MANAGER_START}, - {"QuietMode", 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_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}, - {"HyundaiRadar", PERSISTENT}, + {"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}}, + {"HyundaiRadar", {PERSISTENT | BACKUP, INT, "0"}}, - {"DynamicExperimentalControl", PERSISTENT}, - {"BlindSpot", PERSISTENT | BACKUP}, + {"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}}, // model panel params - {"LagdToggle", 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/pid.py b/common/pid.py index f2ab935f45..721a7c9d65 100644 --- a/common/pid.py +++ b/common/pid.py @@ -17,7 +17,6 @@ class PIDController: self.pos_limit = pos_limit self.neg_limit = neg_limit - self.i_unwind_rate = 0.3 / rate self.i_rate = 1.0 / rate self.speed = 0.0 @@ -35,10 +34,6 @@ class PIDController: def k_d(self): return np.interp(self.speed, self._k_d[0], self._k_d[1]) - @property - def error_integral(self): - return self.i/self.k_i - def reset(self): self.p = 0.0 self.i = 0.0 @@ -46,25 +41,21 @@ class PIDController: self.f = 0.0 self.control = 0 - def update(self, error, error_rate=0.0, speed=0.0, override=False, feedforward=0., freeze_integrator=False): + def update(self, error, error_rate=0.0, speed=0.0, feedforward=0., freeze_integrator=False): self.speed = speed - self.p = float(error) * self.k_p self.f = feedforward * self.k_f self.d = error_rate * self.k_d - if override: - self.i -= self.i_unwind_rate * float(np.sign(self.i)) - else: - if not freeze_integrator: - self.i = self.i + error * self.k_i * self.i_rate + if not freeze_integrator: + i = self.i + error * self.k_i * self.i_rate - # Clip i to prevent exceeding control limits - control_no_i = self.p + self.d + self.f - control_no_i = np.clip(control_no_i, self.neg_limit, self.pos_limit) - self.i = np.clip(self.i, self.neg_limit - control_no_i, self.pos_limit - control_no_i) + # Don't allow windup if already clipping + test_control = self.p + i + self.d + self.f + i_upperbound = self.i if test_control > self.pos_limit else self.pos_limit + i_lowerbound = self.i if test_control < self.neg_limit else self.neg_limit + self.i = np.clip(i, i_lowerbound, i_upperbound) control = self.p + self.i + self.d + self.f - self.control = np.clip(control, self.neg_limit, self.pos_limit) return self.control 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/common/version.h b/common/version.h index 67b69da7c7..669f303211 100644 --- a/common/version.h +++ b/common/version.h @@ -1 +1 @@ -#define COMMA_VERSION "0.9.10" +#define COMMA_VERSION "0.10.1" diff --git a/conftest.py b/conftest.py index b62a88c188..0bd638b87d 100644 --- a/conftest.py +++ b/conftest.py @@ -2,7 +2,6 @@ import contextlib import gc import os import pytest -import random from openpilot.common.prefix import OpenpilotPrefix from openpilot.system.manager import manager @@ -49,8 +48,6 @@ def clean_env(): @pytest.fixture(scope="function", autouse=True) def openpilot_function_fixture(request): - random.seed(0) - with clean_env(): # setup a clean environment for each test with OpenpilotPrefix(shared_download_cache=request.node.get_closest_marker("shared_download_cache") is not None) as prefix: diff --git a/docs/CARS.md b/docs/CARS.md index c53a30caed..acd68ace84 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,322 +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. -# 312 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 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|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|0 mph|32 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 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 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 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/panda b/panda index c33cfa0803..0e7a3fd8cf 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit c33cfa0803deef5dc53d103ae1a2513e2ab94196 +Subproject commit 0e7a3fd8cf6f7a08b80ca15cea1b3989fc11b135 diff --git a/pyproject.toml b/pyproject.toml index 68e5e35296..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", @@ -81,15 +80,13 @@ docs = [ ] testing = [ - "coverage", "hypothesis ==6.47.*", "mypy", "pytest", - "pytest-cov", "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", @@ -123,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] @@ -155,6 +151,7 @@ markers = [ testpaths = [ "common", "selfdrive", + "system/manager", "system/updated", "system/athena", "system/camerad", @@ -180,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/", @@ -265,9 +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" - -[tool.coverage.run] -concurrency = ["multiprocessing", "thread"] +"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/ci/install_github_runner.sh b/release/ci/install_github_runner.sh index f6b1e217c5..9f11e4841c 100755 --- a/release/ci/install_github_runner.sh +++ b/release/ci/install_github_runner.sh @@ -5,6 +5,7 @@ set -e DEFAULT_REPO_URL="https://github.com/sunnypilot" START_AT_BOOT=false RESTORE_MODE=false +RUNNER_VERSION="2.325.0" # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -108,9 +109,9 @@ setup_system_configs() { install_runner() { echo "Downloading and setting up runner..." cd "$RUNNER_DIR" - curl -o actions-runner-linux-arm64-2.322.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.322.0/actions-runner-linux-arm64-2.322.0.tar.gz - sudo -u ${RUNNER_USER} tar -xzf ./actions-runner-linux-arm64-2.322.0.tar.gz - sudo rm ./actions-runner-linux-arm64-2.322.0.tar.gz + curl -o actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz + sudo -u ${RUNNER_USER} tar -xzf ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz + sudo rm ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz sudo chmod +x ./config.sh } @@ -149,25 +150,32 @@ EOL } install_service() { + local service_name + if [ -f "${RUNNER_DIR}/.service" ]; then + service_name=$(cat "${RUNNER_DIR}/.service") + else + service_name="actions.runner.sunnypilot.$(uname -n)" + fi + + create_service_template remount_rw + local service_path="/etc/systemd/system/${service_name}" echo "Installing systemd service..." + if [ -f "${service_path}" ]; then + echo "Service ${service_path} found in systemd, we will delete it" + sudo rm -f "${service_path}" + fi + cd "$RUNNER_DIR" sudo ./svc.sh install $RUNNER_USER if [ "$START_AT_BOOT" = false ]; then - local service_name - if [ -f "${RUNNER_DIR}/.service" ]; then - service_name=$(cat "${RUNNER_DIR}/.service") - else - service_name="actions.runner.sunnypilot.$(uname -n)" - fi sudo systemctl disable "${service_name}" fi remount_ro } check_restore_prerequisites() { - local needs_restore=false local can_restore=false local service_name="" @@ -189,25 +197,16 @@ check_restore_prerequisites() { exit 1 fi - # Then check if restoration is needed (if either service or user is missing) - if ! systemctl list-unit-files "${service_name}" &>/dev/null; then - echo "Service ${service_name} not found in systemd" - needs_restore=true - fi - if ! id "${RUNNER_USER}" &>/dev/null; then echo "User ${RUNNER_USER} does not exist" - needs_restore=true fi # Only proceed if we can restore AND need to restore - if [ "$can_restore" = true ] && [ "$needs_restore" = true ]; then - echo "Restoration is needed and possible" + if [ "$can_restore" = true ]; then + echo "Restoration is possible" return 0 else - if [ "$needs_restore" = false ]; then - echo "System is already properly configured (user and service exist)" - fi + echo "No restoration possible" exit 0 fi } @@ -226,7 +225,6 @@ perform_install() { setup_system_configs install_runner set_directory_permissions - create_service_template configure_runner install_service echo "Installation completed successfully" diff --git a/release/ci/model_generator.py b/release/ci/model_generator.py index 069b8e41af..ee41343be8 100755 --- a/release/ci/model_generator.py +++ b/release/ci/model_generator.py @@ -13,17 +13,32 @@ def create_short_name(full_name): words = [re.sub(r'[^a-zA-Z0-9]', '', word) for word in clean_name.split() if re.sub(r'[^a-zA-Z0-9]', '', word)] if len(words) == 1: - # If there's only one word, return it as is, lowercased, truncated to 8 characters - truncated = words[0][:8] - return truncated.lower() if truncated.isupper() else truncated + return words[0][:8].upper() # Handle special case: Name + Version (e.g., "Word A1" -> "WordA1") if len(words) == 2 and re.match(r'^[A-Za-z]\d+$', words[1]): - first_word = words[0].lower() if words[0].isupper() else words[0] - return (first_word + words[1])[:8] + return (words[0] + words[1])[:8].upper() - # Normal case: first letter and trailing numbers from each word - result = ''.join(word if word.isdigit() else word[0] + (re.search(r'\d+$', word) or [''])[0] for word in words) + result = "" + for word in words: + # Version or number patterns + if (re.match(r'^\d+[a-zA-Z]+$', word) or + re.match(r'^\d+[vVbB]\d+$', word) or + re.match(r'^[vVbB]\d+$', word) or + re.match(r'^\d{4}$', word)): + result += word.upper() + # All uppercase abbreviations (2-3 letters) + elif re.match(r'^[A-Z]{2,3}$', word): + result += word + # Letters+digits (for example tr15 rev2) + elif re.match(r'^[a-zA-Z]+[0-9]+$', word): + result += word[0].upper() + ''.join(re.findall(r'\d+', word)) + elif word.isalpha(): + result += word[0].upper() + elif word.isdigit(): + result += word + else: + result += word[0].upper() return result[:8] @@ -58,14 +73,14 @@ def generate_metadata(model_path: Path, output_dir: Path, short_name: str): "artifact": { "file_name": tinygrad_file.name, "download_uri": { - "url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/", + "url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/", "sha256": tinygrad_hash } }, "metadata": { "file_name": metadata_file.name, "download_uri": { - "url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/", + "url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/", "sha256": metadata_hash } } @@ -83,12 +98,12 @@ def create_metadata_json(models: list, output_dir: Path, custom_name=None, short "ref": upstream_branch, "environment": "development", "runner": "tinygrad", - "build_time": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"), - "models": models, - "overrides": {}, "index": -1, "minimum_selector_version": "-1", "generation": "-1", + "build_time": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"), + "overrides": {}, + "models": models, } # Write metadata to output_dir diff --git a/release/ci/squash_and_merge_prs.py b/release/ci/squash_and_merge_prs.py index b26b08d500..0f20f4f900 100755 --- a/release/ci/squash_and_merge_prs.py +++ b/release/ci/squash_and_merge_prs.py @@ -12,7 +12,7 @@ TRUST_FORK_LABEL = "trust-fork-pr" def setup_argument_parser(): parser = argparse.ArgumentParser(description='Process and squash GitHub PRs') parser.add_argument('--pr-data', type=str, help='PR data in JSON format') - parser.add_argument('--source-branch', type=str, default='master-new', + parser.add_argument('--source-branch', type=str, default='master', help='Source branch for merging') parser.add_argument('--target-branch', type=str, default='master-dev-c3-new-test', help='Target branch for merging') 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/assets/icons/link.png b/selfdrive/assets/icons/link.png new file mode 100644 index 0000000000..8a795c05b8 --- /dev/null +++ b/selfdrive/assets/icons/link.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a69514fe68dd1b4c73f28771640dcba10fc40989d1c7e771cb48bfd830fef206 +size 5713 diff --git a/selfdrive/assets/icons/microphone.png b/selfdrive/assets/icons/microphone.png new file mode 100644 index 0000000000..6cb9cc0254 --- /dev/null +++ b/selfdrive/assets/icons/microphone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fc1f7f31d41f26ea7d6f52b3096f7a91844a3b897bc233a8489253c46f0403b +size 6324 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/helpers.py b/selfdrive/car/helpers.py index 6187be1474..275c84479f 100644 --- a/selfdrive/car/helpers.py +++ b/selfdrive/car/helpers.py @@ -58,5 +58,7 @@ def convert_carControlSP(struct: capnp.lib.capnp._DynamicStructReader) -> struct struct_dataclass.mads = structs.ModularAssistiveDrivingSystem(**remove_deprecated(struct_dict.get('mads', {}))) struct_dataclass.params = [structs.CarControlSP.Param(**remove_deprecated(p)) for p in struct_dict.get('params', [])] + struct_dataclass.leadOne = structs.LeadData(**remove_deprecated(struct_dict.get('leadOne', {}))) + struct_dataclass.leadTwo = structs.LeadData(**remove_deprecated(struct_dict.get('leadTwo', {}))) return struct_dataclass 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..4f61ad3c35 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: diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 04d79d3330..56a3258a5c 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) @@ -48,7 +51,7 @@ class Controls(ControlsExt): poll='selfdriveState') self.pm = messaging.PubMaster(['carControl', 'controlsState'] + self.pm_services_ext) - self.steer_limited_by_controls = False + self.steer_limited_by_safety = False self.curvature = 0.0 self.desired_curvature = 0.0 @@ -93,7 +96,9 @@ class Controls(ControlsExt): torque_params.frictionCoefficientFiltered) self.LaC.extension.update_model_v2(self.sm['modelV2']) - self.LaC.extension.update_lateral_lag(self.sm['liveDelay'].lateralDelay) + + 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'] @@ -135,14 +140,14 @@ class Controls(ControlsExt): actuators.curvature = self.desired_curvature steer, steeringAngleDeg, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp, - self.steer_limited_by_controls, self.desired_curvature, + self.steer_limited_by_safety, self.desired_curvature, self.calibrated_pose, curvature_limited) # TODO what if not available actuators.torque = float(steer) actuators.steeringAngleDeg = float(steeringAngleDeg) # 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): @@ -163,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) @@ -185,10 +187,10 @@ class Controls(ControlsExt): if self.sm['selfdriveState'].active: CO = self.sm['carOutput'] if self.CP.steerControlType == car.CarParams.SteerControlType.angle: - self.steer_limited_by_controls = abs(CC.actuators.steeringAngleDeg - CO.actuatorsOutput.steeringAngleDeg) > \ + self.steer_limited_by_safety = abs(CC.actuators.steeringAngleDeg - CO.actuatorsOutput.steeringAngleDeg) > \ STEER_ANGLE_SATURATION_THRESHOLD else: - self.steer_limited_by_controls = abs(CC.actuators.torque - CO.actuatorsOutput.torque) > 1e-2 + self.steer_limited_by_safety = abs(CC.actuators.torque - CO.actuatorsOutput.torque) > 1e-2 # TODO: both controlsState and carControl valids should be set by # sm.all_checks(), but this creates a circular dependency 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 56eeac3bdf..e28fa3021c 100644 --- a/selfdrive/controls/lib/drive_helpers.py +++ b/selfdrive/controls/lib/drive_helpers.py @@ -1,6 +1,5 @@ import numpy as np -from cereal import log -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 @@ -40,14 +39,6 @@ def clip_curvature(v_ego, prev_curvature, new_curvature, roll): return float(new_curvature), limited_accel or limited_max_curv -def get_speed_error(modelV2: log.ModelDataV2, v_ego: float) -> float: - # ToDo: Try relative error, and absolute speed - if len(modelV2.temporalPose.trans): - vel_err = np.clip(modelV2.temporalPose.trans[0] - v_ego, -MAX_VEL_ERR, MAX_VEL_ERR) - return float(vel_err) - return 0.0 - - def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0.05): if len(speeds) == len(t_idxs): v_now = speeds[0] diff --git a/selfdrive/controls/lib/latcontrol.py b/selfdrive/controls/lib/latcontrol.py index a2c28ed8ce..615168a262 100644 --- a/selfdrive/controls/lib/latcontrol.py +++ b/selfdrive/controls/lib/latcontrol.py @@ -15,15 +15,15 @@ class LatControl(ABC): self.steer_max = 1.0 @abstractmethod - def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, calibrated_pose, curvature_limited): + def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited): pass def reset(self): self.sat_count = 0. - def _check_saturation(self, saturated, CS, steer_limited_by_controls, curvature_limited): + def _check_saturation(self, saturated, CS, steer_limited_by_safety, curvature_limited): # Saturated only if control output is not being limited by car torque/angle rate limits - if (saturated or curvature_limited) and CS.vEgo > self.sat_check_min_speed and not steer_limited_by_controls and not CS.steeringPressed: + if (saturated or curvature_limited) and CS.vEgo > self.sat_check_min_speed and not steer_limited_by_safety and not CS.steeringPressed: self.sat_count += self.sat_count_rate else: self.sat_count -= self.sat_count_rate diff --git a/selfdrive/controls/lib/latcontrol_angle.py b/selfdrive/controls/lib/latcontrol_angle.py index fe6275d9d0..787bd2dbe1 100644 --- a/selfdrive/controls/lib/latcontrol_angle.py +++ b/selfdrive/controls/lib/latcontrol_angle.py @@ -3,6 +3,7 @@ import math from cereal import log from openpilot.selfdrive.controls.lib.latcontrol import LatControl +# TODO This is speed dependent STEER_ANGLE_SATURATION_THRESHOLD = 2.5 # Degrees @@ -10,9 +11,9 @@ class LatControlAngle(LatControl): def __init__(self, CP, CP_SP, CI): super().__init__(CP, CP_SP, CI) self.sat_check_min_speed = 5. - self.use_steer_limited_by_controls = CP.brand == "tesla" + self.use_steer_limited_by_safety = CP.brand == "tesla" - def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, calibrated_pose, curvature_limited): + def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited): angle_log = log.ControlsState.LateralAngleState.new_message() if not active: @@ -23,9 +24,9 @@ class LatControlAngle(LatControl): angle_steers_des = math.degrees(VM.get_steer_from_curvature(-desired_curvature, CS.vEgo, params.roll)) angle_steers_des += params.angleOffsetDeg - if self.use_steer_limited_by_controls: + if self.use_steer_limited_by_safety: # these cars' carcontrollers calculate max lateral accel and jerk, so we can rely on carOutput for saturation - angle_control_saturated = steer_limited_by_controls + angle_control_saturated = steer_limited_by_safety else: # for cars which use a method of limiting torque such as a torque signal (Nissan and Toyota) # or relying on EPS (Ford Q3), carOutput does not capture maxing out torque # TODO: this can be improved diff --git a/selfdrive/controls/lib/latcontrol_pid.py b/selfdrive/controls/lib/latcontrol_pid.py index eb8a1ed5f9..fd79c29bd1 100644 --- a/selfdrive/controls/lib/latcontrol_pid.py +++ b/selfdrive/controls/lib/latcontrol_pid.py @@ -13,11 +13,7 @@ class LatControlPID(LatControl): k_f=CP.lateralTuning.pid.kf, pos_limit=self.steer_max, neg_limit=-self.steer_max) self.get_steer_feedforward = CI.get_steer_feedforward_function() - def reset(self): - super().reset() - self.pid.reset() - - def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, calibrated_pose, curvature_limited): + def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited): pid_log = log.ControlsState.LateralPIDState.new_message() pid_log.steeringAngleDeg = float(CS.steeringAngleDeg) pid_log.steeringRateDeg = float(CS.steeringRateDeg) @@ -29,20 +25,24 @@ class LatControlPID(LatControl): pid_log.steeringAngleDesiredDeg = angle_steers_des pid_log.angleError = error if not active: - output_steer = 0.0 + output_torque = 0.0 pid_log.active = False - self.pid.reset() + else: # offset does not contribute to resistive torque - steer_feedforward = self.get_steer_feedforward(angle_steers_des_no_offset, CS.vEgo) + ff = self.get_steer_feedforward(angle_steers_des_no_offset, CS.vEgo) + freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 + + output_torque = self.pid.update(error, + feedforward=ff, + speed=CS.vEgo, + freeze_integrator=freeze_integrator) - output_steer = self.pid.update(error, override=CS.steeringPressed, - feedforward=steer_feedforward, speed=CS.vEgo) pid_log.active = True pid_log.p = float(self.pid.p) pid_log.i = float(self.pid.i) pid_log.f = float(self.pid.f) - pid_log.output = float(output_steer) - pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_steer) < 1e-3, CS, steer_limited_by_controls, curvature_limited)) + pid_log.output = float(output_torque) + pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_safety, curvature_limited)) - return output_steer, angle_steers_des, pid_log + return output_torque, angle_steers_des, pid_log diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 5edf4cf91b..a73edd173c 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 @@ -31,7 +32,6 @@ class LatControlTorque(LatControl): self.pid = PIDController(self.torque_params.kp, self.torque_params.ki, k_f=self.torque_params.kf, pos_limit=self.steer_max, neg_limit=-self.steer_max) self.torque_from_lateral_accel = CI.torque_from_lateral_accel() - self.use_steering_angle = self.torque_params.useSteeringAngle self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg self.extension = LatControlTorqueExt(self, CP, CP_SP) @@ -41,22 +41,15 @@ class LatControlTorque(LatControl): self.torque_params.latAccelOffset = latAccelOffset self.torque_params.friction = friction - def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, calibrated_pose, curvature_limited): + def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited): pid_log = log.ControlsState.LateralTorqueState.new_message() if not active: output_torque = 0.0 pid_log.active = False else: - actual_curvature_vm = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) + actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY - if self.use_steering_angle: - actual_curvature = actual_curvature_vm - curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) - else: - assert calibrated_pose is not None - actual_curvature_pose = calibrated_pose.angular_velocity.yaw / CS.vEgo - actual_curvature = np.interp(CS.vEgo, [2.0, 5.0], [actual_curvature_vm, actual_curvature_pose]) - curvature_deadzone = 0.0 + curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) desired_lateral_accel = desired_curvature * CS.vEgo ** 2 # desired rate is the desired rate of change in the setpoint, not the absolute desired curvature @@ -69,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 @@ -83,7 +76,7 @@ class LatControlTorque(LatControl): desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel, desired_curvature, actual_curvature) - freeze_integrator = steer_limited_by_controls or CS.steeringPressed or CS.vEgo < 5 + freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 output_torque = self.pid.update(pid_log.error, feedforward=ff, speed=CS.vEgo, @@ -97,7 +90,7 @@ class LatControlTorque(LatControl): pid_log.output = float(-output_torque) pid_log.actualLateralAccel = float(actual_lateral_accel) pid_log.desiredLateralAccel = float(desired_lateral_accel) - pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_controls, curvature_limited)) + pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_safety, curvature_limited)) # TODO left is positive in this convention return -output_torque, 0.0, pid_log 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 4e842f8d1b..785a97a7a9 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -4,14 +4,14 @@ 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 from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC -from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_speed_error, get_accel_from_plan +from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET from openpilot.common.swaglog import cloudlog @@ -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 @@ -54,6 +54,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL): self.CP = CP self.mpc = LongitudinalMpc(dt=dt) + # TODO remove mpc modes when TR released self.mpc.mode = 'acc' LongitudinalPlannerSP.__init__(self, self.CP, self.mpc) self.fcw = False @@ -63,7 +64,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP): self.a_desired = init_a self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt) self.prev_accel_clip = [ACCEL_MIN, ACCEL_MAX] - self.v_model_error = 0.0 self.output_a_target = 0.0 self.output_should_stop = False @@ -73,12 +73,12 @@ class LongitudinalPlanner(LongitudinalPlannerSP): self.solverExecutionTime = 0.0 @staticmethod - def parse_model(model_msg, model_error): + def parse_model(model_msg): if (len(model_msg.position.x) == ModelConstants.IDX_N and len(model_msg.velocity.x) == ModelConstants.IDX_N and len(model_msg.acceleration.x) == ModelConstants.IDX_N): - x = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.position.x) - model_error * T_IDXS_MPC - v = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.velocity.x) - model_error + x = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.position.x) + v = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.velocity.x) a = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.acceleration.x) j = np.zeros(len(T_IDXS_MPC)) else: @@ -94,9 +94,13 @@ class LongitudinalPlanner(LongitudinalPlannerSP): def update(self, sm): self.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc' + if not self.mlsim: + self.mpc.mode = self.mode LongitudinalPlannerSP.update(self, sm) if dec_mpc_mode := self.get_mpc_mode(): self.mode = dec_mpc_mode + if not self.mlsim: + self.mpc.mode = dec_mpc_mode if len(sm['carControl'].orientationNED) == 3: accel_coast = get_coast_accel(sm['carControl'].orientationNED[1]) @@ -133,9 +137,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): # Prevent divergence, smooth in current v_ego self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego)) - # Compute model v_ego error - self.v_model_error = get_speed_error(sm['modelV2'], v_ego) - x, v, a, j, throttle_prob = self.parse_model(sm['modelV2'], self.v_model_error) + x, v, a, j, throttle_prob = self.parse_model(sm['modelV2']) # Don't clip at low speeds since throttle_prob doesn't account for creep self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED @@ -171,7 +173,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): output_a_target_e2e = sm['modelV2'].action.desiredAcceleration output_should_stop_e2e = sm['modelV2'].action.shouldStop - if self.mode == 'acc': + if self.mode == 'acc' or not self.mlsim: output_a_target = output_a_target_mpc self.output_should_stop = output_should_stop_mpc else: @@ -186,7 +188,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 e6767db473..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 @@ -157,8 +158,7 @@ class LateralLagEstimator: block_count: int = BLOCK_NUM, min_valid_block_count: int = BLOCK_NUM_NEEDED, block_size: int = BLOCK_SIZE, window_sec: float = MOVING_WINDOW_SEC, okay_window_sec: float = MIN_OKAY_WINDOW_SEC, min_recovery_buffer_sec: float = MIN_RECOVERY_BUFFER_SEC, min_vego: float = MIN_VEGO, min_yr: float = MIN_ABS_YAW_RATE, min_ncc: float = MIN_NCC, - max_lat_accel: float = MAX_LAT_ACCEL, max_lat_accel_diff: float = MAX_LAT_ACCEL_DIFF, min_confidence: float = MIN_CONFIDENCE, - enabled: bool = True): + max_lat_accel: float = MAX_LAT_ACCEL, max_lat_accel_diff: float = MAX_LAT_ACCEL_DIFF, min_confidence: float = MIN_CONFIDENCE): self.dt = dt self.window_sec = window_sec self.okay_window_sec = okay_window_sec @@ -173,7 +173,6 @@ class LateralLagEstimator: self.min_confidence = min_confidence self.max_lat_accel = max_lat_accel self.max_lat_accel_diff = max_lat_accel_diff - self.enabled = enabled self.t = 0.0 self.lat_active = False @@ -208,7 +207,7 @@ class LateralLagEstimator: liveDelay = msg.liveDelay valid_mean_lag, valid_std, current_mean_lag, current_std = self.block_avg.get() - if self.enabled and self.block_avg.valid_blocks >= self.min_valid_block_count and not np.isnan(valid_mean_lag) and not np.isnan(valid_std): + if self.block_avg.valid_blocks >= self.min_valid_block_count and not np.isnan(valid_mean_lag) and not np.isnan(valid_std): if valid_std > MAX_LAG_STD: liveDelay.status = log.LiveDelayData.Status.invalid else: @@ -371,14 +370,13 @@ def main(): params = Params() CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) - # TODO: remove me, lagd is in shadow mode on release - is_release = params.get_bool("IsReleaseBranch") - - lag_learner = LateralLagEstimator(CP, 1. / SERVICE_LIST['livePose'].frequency, enabled=not is_release) + lag_learner = LateralLagEstimator(CP, 1. / SERVICE_LIST['livePose'].frequency) if (initial_lag_params := retrieve_initial_lag(params, CP)) is not None: lag, valid_blocks = initial_lag_params lag_learner.reset(lag, valid_blocks) + lagd_toggle = LagdToggle(CP) + while True: sm.update() if sm.all_checks(): @@ -397,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_lagd.py b/selfdrive/locationd/test/test_lagd.py index 8fb829edd8..a3dfce9c29 100644 --- a/selfdrive/locationd/test/test_lagd.py +++ b/selfdrive/locationd/test/test_lagd.py @@ -110,19 +110,6 @@ class TestLagd: assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED assert msg.liveDelay.calPerc == 100 - def test_disabled_estimator(self): - mocked_CP = car.CarParams(steerActuatorDelay=0.8) - estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, enabled=False) - lag_frames = 5 - process_messages(estimator, lag_frames, int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_NUM_NEEDED * BLOCK_SIZE) - msg = estimator.get_msg(True) - assert msg.liveDelay.status == 'unestimated' - assert np.allclose(msg.liveDelay.lateralDelay, 1.0, atol=0.01) - assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01) - assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01) - assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED - assert msg.liveDelay.calPerc == 100 - def test_estimator_masking(self): mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(1, 19) estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, min_valid_block_count=1) 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 23bd99931b..e0409957f5 100755 --- a/selfdrive/locationd/torqued.py +++ b/selfdrive/locationd/torqued.py @@ -4,12 +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.helpers import get_lat_delay HISTORY = 5 # secs POINTS_PER_BUCKET = 1500 @@ -32,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,6 +52,8 @@ class TorqueBuckets(PointBuckets): class TorqueEstimator(ParameterEstimator): def __init__(self, CP, decimated=False, track_all_points=False): + super().__init__() + self.CP = CP self.hist_len = int(HISTORY / DT_MDL) self.lag = 0.0 self.track_all_points = track_all_points # for offline analysis, without max lateral accel or max steer torque filters @@ -95,6 +98,7 @@ class TorqueEstimator(ParameterEstimator): # 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: @@ -176,7 +180,7 @@ class TorqueEstimator(ParameterEstimator): elif which == "liveCalibration": self.calibrator.feed_live_calib(msg) elif which == "liveDelay": - self.lag = msg.lateralDelay + 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 545fcdcff7..6802b45e6e 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -50,23 +50,21 @@ 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 -import subprocess -from tinygrad import Device - -# because tg doesn't support multi-process -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: - del Device - print("USB GPU detected... building") - flags = "AMD=1 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 -else: - print("USB GPU not detected... skipping") +if "USBGPU" in os.environ: + import subprocess + # because tg doesn't support multi-process + 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 = "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 + else: + print("USB GPU not detected... skipping") diff --git a/selfdrive/modeld/dmonitoringmodeld.py b/selfdrive/modeld/dmonitoringmodeld.py index 2a3df3f652..215056e6e6 100755 --- a/selfdrive/modeld/dmonitoringmodeld.py +++ b/selfdrive/modeld/dmonitoringmodeld.py @@ -1,20 +1,15 @@ #!/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 import ctypes import numpy as np from pathlib import Path -from setproctitle import setproctitle from cereal import messaging from cereal.messaging import PubMaster, SubMaster @@ -25,7 +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.system import sentry +from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address MODEL_WIDTH, MODEL_HEIGHT = DM_INPUT_SIZE CALIB_LEN = 3 @@ -95,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 @@ -133,12 +128,8 @@ def get_driverstate_packet(model_output: np.ndarray, frame_id: int, location_ts: def main(): - setproctitle(PROCESS_NAME) config_realtime_process(7, 5) - sentry.set_tag("daemon", PROCESS_NAME) - cloudlog.bind(daemon=PROCESS_NAME) - cl_context = CLContext() model = ModelState(cl_context) cloudlog.warning("models loaded, dmonitoringmodeld starting") @@ -180,7 +171,4 @@ if __name__ == "__main__": try: main() except KeyboardInterrupt: - cloudlog.warning(f"child {PROCESS_NAME} got SIGINT") - except Exception: - sentry.capture_exception() - raise + cloudlog.warning("got SIGINT") diff --git a/selfdrive/modeld/fill_model_msg.py b/selfdrive/modeld/fill_model_msg.py index a91c6395c7..a2b54b420e 100644 --- a/selfdrive/modeld/fill_model_msg.py +++ b/selfdrive/modeld/fill_model_msg.py @@ -89,13 +89,6 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D fill_xyzt(modelV2.orientation, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.T_FROM_CURRENT_EULER].T) fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) - # temporal pose - temporal_pose = modelV2.temporalPose - temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist() - temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist() - temporal_pose.rot = net_output_data['sim_pose'][0,ModelConstants.POSE_WIDTH//2:].tolist() - temporal_pose.rotStd = net_output_data['sim_pose_stds'][0,ModelConstants.POSE_WIDTH//2:].tolist() - # poly path fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index ac88dfbb5f..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 @@ -19,7 +14,6 @@ import numpy as np import cereal.messaging as messaging from cereal import car, log from pathlib import Path -from setproctitle import setproctitle from cereal.messaging import PubMaster, SubMaster from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from opendbc.car.car_helpers import get_demo_car_params @@ -29,13 +23,16 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.realtime import config_realtime_process, DT_MDL from openpilot.common.transformations.camera import DEVICE_CAMERAS from openpilot.common.transformations.model import get_warp_matrix -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.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan 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.helpers import get_lat_delay +from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase PROCESS_NAME = "selfdrive.modeld.modeld" @@ -46,8 +43,8 @@ POLICY_PKL_PATH = Path(__file__).parent / 'models/driving_policy_tinygrad.pkl' VISION_METADATA_PATH = Path(__file__).parent / 'models/driving_vision_metadata.pkl' POLICY_METADATA_PATH = Path(__file__).parent / 'models/driving_policy_metadata.pkl' -LAT_SMOOTH_SECONDS = 0.0 -LONG_SMOOTH_SECONDS = 0.0 +LAT_SMOOTH_SECONDS = 0.1 +LONG_SMOOTH_SECONDS = 0.3 MIN_LAT_CONTROL_SPEED = 0.3 @@ -60,7 +57,11 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log. action_t=long_action_t) desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, LONG_SMOOTH_SECONDS) - desired_curvature = model_output['desired_curvature'][0, 0] + desired_curvature = get_curvature_from_plan(plan[:,Plan.T_FROM_CURRENT_EULER][:,2], + plan[:,Plan.ORIENTATION_RATE][:,2], + ModelConstants.T_IDXS, + v_ego, + lat_action_t) if v_ego > MIN_LAT_CONTROL_SPEED: desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, LAT_SMOOTH_SECONDS) else: @@ -79,13 +80,15 @@ 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) self.vision_input_shapes = vision_metadata['input_shapes'] @@ -161,20 +164,20 @@ 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 self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:] self.full_prev_desired_curv[0,-1,:] = policy_outputs_dict['desired_curvature'][0, :] - self.numpy_inputs['prev_desired_curv'][:] = self.full_prev_desired_curv[0, self.temporal_idxs] + self.numpy_inputs['prev_desired_curv'][:] = 0*self.full_prev_desired_curv[0, self.temporal_idxs] combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict} if SEND_RAW_PRED: @@ -186,9 +189,6 @@ class ModelState: def main(demo=False): cloudlog.warning("modeld init") - sentry.set_tag("daemon", PROCESS_NAME) - cloudlog.bind(daemon=PROCESS_NAME) - setproctitle(PROCESS_NAME) if not USBGPU: # USB GPU currently saturates a core so can't do this yet, # also need to move the aux USB interrupts for good timings @@ -296,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 = sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS + 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) @@ -374,7 +376,4 @@ if __name__ == "__main__": args = parser.parse_args() main(demo=args.demo) except KeyboardInterrupt: - cloudlog.warning(f"child {PROCESS_NAME} got SIGINT") - except Exception: - sentry.capture_exception() - raise + cloudlog.warning("got SIGINT") diff --git a/selfdrive/modeld/models/driving_policy.onnx b/selfdrive/modeld/models/driving_policy.onnx index 343c3a0aa8..267fc92a3f 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:5f714fb38bcd44b5d9f44e3a8e1595e1dfdf7558f0eec3485cf3f2dbb6dc7d8d -size 15971805 +oid sha256:1af87c38492444521632a0e75839b5684ee46bf255b3474773784bffb9fe4f57 +size 15583374 diff --git a/selfdrive/modeld/models/driving_vision.onnx b/selfdrive/modeld/models/driving_vision.onnx index 924d11304d..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:3ac4867fbc618037e8d03143edbfeeae960f2025644b5dcf36c6665271b4f874 -size 34883375 +oid sha256:c824f68646a3b94f117f01c70dc8316fb466e05fbd42ccdba440b8a8dc86914b +size 46265993 diff --git a/selfdrive/modeld/parse_model_outputs.py b/selfdrive/modeld/parse_model_outputs.py index 810c44ccb9..9e1c048735 100644 --- a/selfdrive/modeld/parse_model_outputs.py +++ b/selfdrive/modeld/parse_model_outputs.py @@ -88,24 +88,29 @@ class Parser: self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) 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_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) + if outs['lead'].shape[1] == 2 * ModelConstants.LEAD_MHP_SELECTION *ModelConstants.LEAD_TRAJ_LEN * ModelConstants.LEAD_WIDTH: + self.parse_mdn('lead', outs, in_N=0, out_N=0, + out_shape=(ModelConstants.LEAD_MHP_SELECTION, ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) + else: + 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)) - 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('sim_pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_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)) - 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 outs['plan'].shape[1] == 2 * ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH: + self.parse_mdn('plan', outs, in_N=0, out_N=0, + out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) + else: + 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)) if 'desired_curvature' in outs: self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,)) - for k in ['lead_prob', 'lane_lines_prob']: - self.parse_binary_crossentropy(k, outs) self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) return 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 dfda2c31f7..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) @@ -135,7 +143,8 @@ class SelfdriveD(CruiseHelper): self.ignored_processes.update({'mapd'}) # Determine startup event - self.startup_event = EventName.startup if build_metadata.openpilot.comma_remote and build_metadata.tested_channel else EventName.startupMaster + is_remote = build_metadata.openpilot.comma_remote or build_metadata.openpilot.sunnypilot_remote + self.startup_event = EventName.startup if is_remote and build_metadata.tested_channel else EventName.startupMaster if not car_recognized: self.startup_event = EventName.startupNoCar elif car_recognized and self.CP.passive: @@ -182,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: @@ -253,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 @@ -304,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: @@ -406,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) @@ -537,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 f839238098..54ff189358 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -f440c9e0469d32d350aa99ddaa8f44591a2ce690 \ No newline at end of file +543bd2347fa35f8300478a3893fdd0a03a7c1fe6 \ 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 4a29f408ee..674e5005f4 100644 --- a/selfdrive/ui/layouts/settings/settings.py +++ b/selfdrive/ui/layouts/settings/settings.py @@ -2,19 +2,20 @@ 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 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 +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 @@ -27,7 +28,7 @@ PANEL_COLOR = rl.Color(41, 41, 41, 255) CLOSE_BTN_COLOR = rl.Color(41, 41, 41, 255) CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255) TEXT_NORMAL = rl.Color(128, 128, 128, 255) -TEXT_SELECTED = rl.Color(255, 255, 255, 255) +TEXT_SELECTED = rl.WHITE class PanelType(IntEnum): @@ -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 @@ -132,7 +132,7 @@ class SettingsLayout(Widget): if panel.instance: panel.instance.render(content_rect) - def _handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool: + def _handle_mouse_release(self, mouse_pos: MousePos) -> bool: # Check close button if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect): if self._close_callback: 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 5c17082769..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 = { @@ -22,6 +22,11 @@ DESCRIPTIONS = { "AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.", '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." + ), } @@ -53,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( @@ -75,7 +80,19 @@ class TogglesLayout(Widget): icon="monitoring.png", ), toggle_item( - "Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="monitoring.png" + "Record Microphone Audio", + DESCRIPTIONS["RecordAudio"], + self._params.get_bool("RecordAudio"), + icon="microphone.png", + ), + toggle_item( + "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" ), ] @@ -85,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 13f69b79d4..cdbfa4c04c 100644 --- a/selfdrive/ui/layouts/sidebar.py +++ b/selfdrive/ui/layouts/sidebar.py @@ -4,14 +4,15 @@ from dataclasses import dataclass from collections.abc import Callable from cereal import log from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import gui_app, FontWeight +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) @@ -23,18 +24,18 @@ NetworkType = log.DeviceState.NetworkType # Color scheme class Colors: SIDEBAR_BG = rl.Color(57, 57, 57, 255) - WHITE = rl.Color(255, 255, 255, 255) + WHITE = rl.WHITE WHITE_DIM = rl.Color(255, 255, 255, 85) GRAY = rl.Color(84, 84, 84, 255) # Status colors - GOOD = rl.Color(255, 255, 255, 255) + GOOD = rl.WHITE WARNING = rl.Color(218, 202, 37, 255) DANGER = rl.Color(201, 34, 49, 255) # UI elements METRIC_BORDER = rl.Color(255, 255, 255, 85) - BUTTON_NORMAL = rl.Color(255, 255, 255, 255) + BUTTON_NORMAL = rl.WHITE BUTTON_PRESSED = rl.Color(255, 255, 255, 166) @@ -135,7 +136,7 @@ class Sidebar(Widget): else: self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD) - def _handle_mouse_release(self, mouse_pos: rl.Vector2): + def _handle_mouse_release(self, mouse_pos: MousePos): if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN): if self._on_settings_click: self._on_settings_click() @@ -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..536d993389 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 @@ -34,7 +34,7 @@ class FontSizes: @dataclass(frozen=True) class Colors: - white: rl.Color = rl.Color(255, 255, 255, 255) + white: rl.Color = rl.WHITE disengaged: rl.Color = rl.Color(145, 155, 149, 255) override: rl.Color = rl.Color(145, 155, 149, 255) # Added engaged: rl.Color = rl.Color(128, 216, 166, 255) @@ -47,7 +47,7 @@ class Colors: white_translucent: rl.Color = rl.Color(255, 255, 255, 200) border_translucent: rl.Color = rl.Color(255, 255, 255, 75) header_gradient_start: rl.Color = rl.Color(0, 0, 0, 114) - header_gradient_end: rl.Color = rl.Color(0, 0, 0, 0) + header_gradient_end: rl.Color = rl.BLANK UI_CONFIG = UIConfig() 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/developer_panel.cc b/selfdrive/ui/qt/offroad/developer_panel.cc index 3e9647d2cf..fbfb16294b 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.cc +++ b/selfdrive/ui/qt/offroad/developer_panel.cc @@ -45,17 +45,6 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { }); addItem(experimentalLongitudinalToggle); - enableGithubRunner = new ParamControl("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), ""); - addItem(enableGithubRunner); - - // error log button - errorLogBtn = new ButtonControl(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes.")); - connect(errorLogBtn, &ButtonControl::clicked, [=]() { - std::string txt = util::read_file("/data/community/crashes/error.log"); - ConfirmationDialog::rich(QString::fromStdString(txt), this); - }); - addItem(errorLogBtn); - // Joystick and longitudinal maneuvers should be hidden on release branches is_release = params.getBool("IsReleaseBranch"); @@ -104,8 +93,6 @@ void DeveloperPanel::updateToggles(bool _offroad) { experimentalLongitudinalToggle->refresh(); // Handle specific controls visibility for release branches - enableGithubRunner->setVisible(!is_release); - errorLogBtn->setVisible(!is_release); joystickToggle->setVisible(!is_release); offroad = _offroad; diff --git a/selfdrive/ui/qt/offroad/developer_panel.h b/selfdrive/ui/qt/offroad/developer_panel.h index c15b574f5a..51b1b0b6f0 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.h +++ b/selfdrive/ui/qt/offroad/developer_panel.h @@ -16,10 +16,8 @@ private: Params params; ParamControl* adbToggle; ParamControl* joystickToggle; - ButtonControl* errorLogBtn; ParamControl* longManeuverToggle; ParamControl* experimentalLongitudinalToggle; - ParamControl* enableGithubRunner; bool is_release; bool offroad = false; diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 6b97342966..f8a46257f6 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -68,6 +68,20 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { "../assets/icons/monitoring.png", true, }, + { + "RecordAudio", + tr("Record and Upload Microphone Audio"), + tr("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."), + "../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"), @@ -342,26 +356,22 @@ void DevicePanel::updateCalibDescription() { } } - const bool is_release = params.getBool("IsReleaseBranch"); - if (!is_release) { - int lag_perc = 0; - std::string lag_bytes = params.get("LiveDelay"); - if (!lag_bytes.empty()) { - try { - AlignedBuffer aligned_buf; - capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size())); - lag_perc = cmsg.getRoot().getLiveDelay().getCalPerc(); - } catch (kj::Exception) { - qInfo() << "invalid LiveDelay"; - } - } - desc += "\n\n"; - if (lag_perc < 100) { - desc += tr("Steering lag calibration is %1% complete.").arg(lag_perc); - } else { - desc += tr("Steering lag calibration is complete."); + int lag_perc = 0; + std::string lag_bytes = params.get("LiveDelay"); + if (!lag_bytes.empty()) { + try { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size())); + lag_perc = cmsg.getRoot().getLiveDelay().getCalPerc(); + } catch (kj::Exception) { + qInfo() << "invalid LiveDelay"; } } + if (lag_perc < 100) { + desc += tr("\n\nSteering lag calibration is %1% complete.").arg(lag_perc); + } else { + desc += tr("\n\nSteering lag calibration is complete."); + } std::string torque_bytes = params.get("LiveTorqueParameters"); if (!torque_bytes.empty()) { @@ -372,11 +382,10 @@ void DevicePanel::updateCalibDescription() { // don't add for non-torque cars if (torque.getUseParams()) { int torque_perc = torque.getCalPerc(); - desc += is_release ? "\n\n" : " "; if (torque_perc < 100) { - desc += tr("Steering torque response calibration is %1% complete.").arg(torque_perc); + desc += tr(" Steering torque response calibration is %1% complete.").arg(torque_perc); } else { - desc += tr("Steering torque response calibration is complete."); + desc += tr(" Steering torque response calibration is complete."); } } } catch (kj::Exception) { diff --git a/selfdrive/ui/qt/onroad/model.cc b/selfdrive/ui/qt/onroad/model.cc index dc801e591f..a1e3756cb6 100644 --- a/selfdrive/ui/qt/onroad/model.cc +++ b/selfdrive/ui/qt/onroad/model.cc @@ -34,6 +34,7 @@ void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) { drawLead(painter, lead_two, lead_vertices[1], surface_rect); } } + drawLeadStatus(painter, surface_rect.height(), surface_rect.width()); painter.restore(); } @@ -173,6 +174,173 @@ QColor ModelRenderer::blendColors(const QColor &start, const QColor &end, float (1 - t) * start.alphaF() + t * end.alphaF()); } + +void ModelRenderer::drawLeadStatus(QPainter &painter, int height, int width) { + auto *s = uiState(); + auto &sm = *(s->sm); + + if (!sm.alive("radarState")) return; + + const auto &radar_state = sm["radarState"].getRadarState(); + const auto &lead_one = radar_state.getLeadOne(); + const auto &lead_two = radar_state.getLeadTwo(); + + // Check if we have any active leads + bool has_lead_one = lead_one.getStatus(); + bool has_lead_two = lead_two.getStatus(); + + if (!has_lead_one && !has_lead_two) { + // Fade out status display + lead_status_alpha = std::max(0.0f, lead_status_alpha - 0.05f); + if (lead_status_alpha <= 0.0f) return; + } else { + // Fade in status display + lead_status_alpha = std::min(1.0f, lead_status_alpha + 0.1f); + } + + // Draw status for each lead vehicle under its chevron + if (true) { + drawLeadStatusAtPosition(painter, lead_one, lead_vertices[0], height, width, "L1"); + } + + if (has_lead_two && std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0) { + drawLeadStatusAtPosition(painter, lead_two, lead_vertices[1], height, width, "L2"); + } +} + +void ModelRenderer::drawLeadStatusAtPosition(QPainter &painter, + const cereal::RadarState::LeadData::Reader &lead_data, + const QPointF &chevron_pos, + int height, int width, + const QString &label) { + + float d_rel = lead_data.getDRel(); + float v_rel = lead_data.getVRel(); + auto *s = uiState(); + auto &sm = *(s->sm); + float v_ego = sm["carState"].getCarState().getVEgo(); + + int chevron_data = std::atoi(Params().get("ChevronInfo").c_str()); + + // Calculate chevron size (same logic as drawLead) + float sz = std::clamp((25 * 30) / (d_rel / 3 + 30), 15.0f, 30.0f) * 2.35; + + QFont content_font = painter.font(); + content_font.setPixelSize(35); + content_font.setBold(true); + painter.setFont(content_font); + + QFontMetrics fm(content_font); + bool is_metric = s->scene.is_metric; + + QStringList text_lines; + + const int chevron_types = 3; + const int chevron_all = chevron_types + 1; // All metrics (value 4) + QStringList chevron_text[chevron_types]; + int position; + float val; + + // Distance display (chevron_data == 1 or all) + if (chevron_data == 1 || chevron_data == chevron_all) { + position = 0; + val = std::max(0.0f, d_rel); + QString distance_unit = is_metric ? "m" : "ft"; + if (!is_metric) { + val *= 3.28084f; // Convert meters to feet + } + chevron_text[position].append(QString::number(val, 'f', 0) + " " + distance_unit); + } + + // Absolute velocity display (chevron_data == 2 or all) + if (chevron_data == 2 || chevron_data == chevron_all) { + position = (chevron_data == 2) ? 0 : 1; + val = std::max(0.0f, (v_rel + v_ego) * (is_metric ? static_cast(MS_TO_KPH) : static_cast(MS_TO_MPH))); + chevron_text[position].append(QString::number(val, 'f', 0) + " " + (is_metric ? "km/h" : "mph")); + } + + // Time-to-contact display (chevron_data == 3 or all) + if (chevron_data == 3 || chevron_data == chevron_all) { + position = (chevron_data == 3) ? 0 : 2; + val = (d_rel > 0 && v_ego > 0) ? std::max(0.0f, d_rel / v_ego) : 0.0f; + QString ttc_str = (val > 0 && val < 200) ? QString::number(val, 'f', 1) + "s" : "---"; + chevron_text[position].append(ttc_str); + } + + // Collect all non-empty text lines + for (int i = 0; i < chevron_types; ++i) { + if (!chevron_text[i].isEmpty()) { + text_lines.append(chevron_text[i]); + } + } + + // If no text to display, return early + if (text_lines.isEmpty()) { + return; + } + + // Text box dimensions + float str_w = 150; // Width of text area + float str_h = 45; // Height per line + + // Position text below chevron, centered horizontally + float text_x = chevron_pos.x() - str_w / 2; + float text_y = chevron_pos.y() + sz + 15; + + // Clamp to screen bounds + text_x = std::clamp(text_x, 10.0f, (float)width - str_w - 10); + + // Shadow offset + QPoint shadow_offset(2, 2); + + // Draw each line of text with shadow + for (int i = 0; i < text_lines.size(); ++i) { + if (!text_lines[i].isEmpty()) { + QRect textRect(text_x, text_y + (i * str_h), str_w, str_h); + + // Draw shadow + painter.setPen(QColor(0x0, 0x0, 0x0, (int)(200 * lead_status_alpha))); + painter.drawText(textRect.translated(shadow_offset.x(), shadow_offset.y()), + Qt::AlignBottom | Qt::AlignHCenter, text_lines[i]); + + // Determine text color based on content and danger level + QColor text_color; + + // Check if this is a distance line (contains 'm' or 'ft') + if (text_lines[i].contains("m") || text_lines[i].contains("ft")) { + if (d_rel < 20.0f) { + text_color = QColor(255, 80, 80, (int)(255 * lead_status_alpha)); // Red - danger + } else if (d_rel < 40.0f) { + text_color = QColor(255, 200, 80, (int)(255 * lead_status_alpha)); // Yellow - caution + } else { + text_color = QColor(80, 255, 120, (int)(255 * lead_status_alpha)); // Green - safe + } + } + // Enhanced color coding for time-to-contact + else if (text_lines[i].contains("s") && !text_lines[i].contains("---")) { + float ttc_val = text_lines[i].left(text_lines[i].length() - 1).toFloat(); + if (ttc_val < 3.0f) { + text_color = QColor(255, 80, 80, (int)(255 * lead_status_alpha)); // Red - urgent + } else if (ttc_val < 6.0f) { + text_color = QColor(255, 200, 80, (int)(255 * lead_status_alpha)); // Yellow - caution + } else { + text_color = QColor(0xff, 0xff, 0xff, (int)(255 * lead_status_alpha)); // White - safe + } + } + else { + text_color = QColor(0xff, 0xff, 0xff, (int)(255 * lead_status_alpha)); // White for other lines + } + + // Draw main text + painter.setPen(text_color); + painter.drawText(textRect, Qt::AlignBottom | Qt::AlignHCenter, text_lines[i]); + } + } + + // Reset pen + painter.setPen(Qt::NoPen); +} + void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd, const QRect &surface_rect) { const float speedBuff = 10.; diff --git a/selfdrive/ui/qt/onroad/model.h b/selfdrive/ui/qt/onroad/model.h index 85eb236e76..e40f832c37 100644 --- a/selfdrive/ui/qt/onroad/model.h +++ b/selfdrive/ui/qt/onroad/model.h @@ -34,6 +34,12 @@ protected: bool mapToScreen(float in_x, float in_y, float in_z, QPointF *out); void mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off, QPolygonF *pvd, int max_idx, bool allow_invert = true); + void drawLeadStatus(QPainter &painter, int height, int width); + void drawLeadStatusAtPosition(QPainter &painter, + const cereal::RadarState::LeadData::Reader &lead_data, + const QPointF &chevron_pos, + int height, int width, + const QString &label); void drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd, const QRect &surface_rect); void update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line); virtual void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead); @@ -58,4 +64,9 @@ protected: QPointF lead_vertices[2] = {}; Eigen::Matrix3f car_space_transform = Eigen::Matrix3f::Zero(); QRectF clip_region; + + float lead_status_alpha = 0.0f; + QPointF lead_status_pos; + QString lead_status_text; + QColor lead_status_color; }; 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 fd8ea5bcf5..72400bd945 100644 --- a/selfdrive/ui/qt/sidebar.cc +++ b/selfdrive/ui/qt/sidebar.cc @@ -24,10 +24,12 @@ void Sidebar::drawMetric(QPainter &p, const QPair &label, QCol p.drawText(rect.adjusted(22, 0, 0, 0), Qt::AlignCenter, label.first + "\n" + label.second); } -Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed(false), settings_pressed(false) { +Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed(false), settings_pressed(false), mic_indicator_pressed(false) { home_img = loadPixmap("../assets/images/button_home.png", home_btn.size()); flag_img = loadPixmap("../assets/images/button_flag.png", home_btn.size()); settings_img = loadPixmap("../assets/images/button_settings.png", settings_btn.size(), Qt::IgnoreAspectRatio); + mic_img = loadPixmap("../assets/icons/microphone.png", QSize(30, 30)); + link_img = loadPixmap("../assets/icons/link.png", QSize(60, 60)); connect(this, &Sidebar::valueChanged, [=] { update(); }); @@ -37,7 +39,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) { @@ -47,20 +49,25 @@ void Sidebar::mousePressEvent(QMouseEvent *event) { } else if (settings_btn.contains(event->pos())) { settings_pressed = true; update(); + } else if (recording_audio && mic_indicator_btn.contains(event->pos())) { + mic_indicator_pressed = true; + update(); } } void Sidebar::mouseReleaseEvent(QMouseEvent *event) { - if (flag_pressed || settings_pressed) { - flag_pressed = settings_pressed = false; + if (flag_pressed || settings_pressed || mic_indicator_pressed) { + flag_pressed = settings_pressed = mic_indicator_pressed = false; update(); } 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())) { + emit openSettings(2, "RecordAudio"); } } @@ -106,6 +113,8 @@ void Sidebar::updateState(const UIState &s) { pandaStatus = {{tr("NO"), tr("PANDA")}, danger_color}; } setProperty("pandaStatus", QVariant::fromValue(pandaStatus)); + + setProperty("recordingAudio", s.scene.recording_audio); } void Sidebar::paintEvent(QPaintEvent *event) { @@ -124,6 +133,14 @@ void Sidebar::drawSidebar(QPainter &p) { p.drawPixmap(settings_btn.x(), settings_btn.y(), settings_img); p.setOpacity(onroad && flag_pressed ? 0.65 : 1.0); p.drawPixmap(home_btn.x(), home_btn.y(), onroad ? flag_img : home_img); + if (recording_audio) { + p.setBrush(danger_color); + p.setOpacity(mic_indicator_pressed ? 0.65 : 1.0); + p.drawRoundedRect(mic_indicator_btn, mic_indicator_btn.height() / 2, mic_indicator_btn.height() / 2); + int icon_x = mic_indicator_btn.x() + (mic_indicator_btn.width() - mic_img.width()) / 2; + int icon_y = mic_indicator_btn.y() + (mic_indicator_btn.height() - mic_img.height()) / 2; + p.drawPixmap(icon_x, icon_y, mic_img); + } p.setOpacity(1.0); // network @@ -138,7 +155,12 @@ void Sidebar::drawSidebar(QPainter &p) { p.setFont(InterFont(35)); p.setPen(QColor(0xff, 0xff, 0xff)); const QRect r = QRect(58, 247, width() - 100, 50); - p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, net_type); + + if (net_type == "Hotspot") { + p.drawPixmap(r.x(), r.y() + (r.height() - link_img.height()) / 2, link_img); + } else { + p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, net_type); + } #ifndef SUNNYPILOT // metrics diff --git a/selfdrive/ui/qt/sidebar.h b/selfdrive/ui/qt/sidebar.h index 3c57d10a1a..30a92c972e 100644 --- a/selfdrive/ui/qt/sidebar.h +++ b/selfdrive/ui/qt/sidebar.h @@ -23,6 +23,7 @@ class Sidebar : public QFrame { Q_PROPERTY(ItemStatus tempStatus MEMBER temp_status NOTIFY valueChanged); Q_PROPERTY(QString netType MEMBER net_type NOTIFY valueChanged); Q_PROPERTY(int netStrength MEMBER net_strength NOTIFY valueChanged); + Q_PROPERTY(bool recordingAudio MEMBER recording_audio NOTIFY valueChanged); public: explicit Sidebar(QWidget* parent = 0); @@ -42,8 +43,8 @@ protected: void drawMetric(QPainter &p, const QPair &label, QColor c, int y); virtual void drawSidebar(QPainter &p); - QPixmap home_img, flag_img, settings_img; - bool onroad, flag_pressed, settings_pressed; + QPixmap home_img, flag_img, settings_img, mic_img, link_img; + bool onroad, recording_audio, flag_pressed, settings_pressed, mic_indicator_pressed; const QMap network_type = { {cereal::DeviceState::NetworkType::NONE, tr("--")}, {cereal::DeviceState::NetworkType::WIFI, tr("Wi-Fi")}, @@ -56,6 +57,7 @@ protected: const QRect home_btn = QRect(60, 860, 180, 180); const QRect settings_btn = QRect(50, 35, 200, 117); + const QRect mic_indicator_btn = QRect(158, 252, 75, 40); const QColor good_color = QColor(255, 255, 255); const QColor warning_color = QColor(218, 202, 37); const QColor danger_color = QColor(201, 34, 49); 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/input.cc b/selfdrive/ui/qt/widgets/input.cc index 0cbf14931b..4f330dca8d 100644 --- a/selfdrive/ui/qt/widgets/input.cc +++ b/selfdrive/ui/qt/widgets/input.cc @@ -2,6 +2,7 @@ #include #include +#include #include "system/hardware/hw.h" #include "selfdrive/ui/qt/util.h" @@ -334,3 +335,141 @@ QString MultiOptionDialog::getSelection(const QString &prompt_text, const QStrin } return ""; } + +TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QList> &items, + const QString ¤t, QWidget *parent) : DialogBase(parent) { + QFrame *container = new QFrame(this); + container->setStyleSheet(R"( + QFrame { background-color: #1B1B1B; } + #confirm_btn[enabled="false"] { background-color: #2B2B2B; } + #confirm_btn:enabled { background-color: #465BEA; } + #confirm_btn:enabled:pressed { background-color: #3049F4; } + QTreeWidget { + background-color: transparent; + border: none; + } + QTreeWidget::item { + height: 135; + padding: 0px 50px; + margin: 5px; + text-align: left; + font-size: 55px; + font-weight: 300; + border-radius: 10px; + background-color: #4F4F4F; + color: white; + } + QTreeWidget::item:selected { + background-color: #465BEA; + } + QTreeWidget::branch { + background-color: transparent; + } + )"); + + QVBoxLayout *main_layout = new QVBoxLayout(container); + main_layout->setContentsMargins(55, 50, 55, 50); + + QLabel *title = new QLabel(prompt_text, this); + title->setStyleSheet("font-size: 70px; font-weight: 500;"); + main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); + main_layout->addSpacing(25); + + treeWidget = new QTreeWidget(this); + treeWidget->setHeaderHidden(true); + treeWidget->setIndentation(50); + treeWidget->setExpandsOnDoubleClick(false); // Disable double-click expansion + treeWidget->setAnimated(true); + treeWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + treeWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + treeWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + treeWidget->setDragEnabled(false); + treeWidget->setMouseTracking(true); + + // Connect single-click to expand/collapse + QObject::connect(treeWidget, &QTreeWidget::itemClicked, [=](QTreeWidgetItem *item, int) { + if (item->childCount() > 0) { + item->setExpanded(!item->isExpanded()); + treeWidget->scrollToItem(item->child(0), QAbstractItemView::EnsureVisible); + } + }); + + QScroller::grabGesture(treeWidget->viewport(), QScroller::LeftMouseButtonGesture); + + // Populate tree + QListIterator> iter(items); + while (iter.hasNext()) { + QPair currItem = iter.next(); + if (currItem.first.isEmpty()) { + for (const QString &item : currItem.second) { + QTreeWidgetItem *topLevel = new QTreeWidgetItem(); + topLevel->setText(0, item); + topLevel->setFlags(topLevel->flags() | Qt::ItemIsSelectable); + treeWidget->addTopLevelItem(topLevel); + if (item == current) { + topLevel->setSelected(true); + } + } + } else { + QTreeWidgetItem *folderItem = new QTreeWidgetItem(treeWidget); + folderItem->setIcon(0, QIcon(QPixmap("../assets/icons/menu.png"))); + folderItem->setText(0, " " + currItem.first); + folderItem->setFlags(folderItem->flags() | Qt::ItemIsAutoTristate); + folderItem->setFlags(folderItem->flags() & ~Qt::ItemIsSelectable); + + for (const QString &item : currItem.second) + { + QTreeWidgetItem *childItem = new QTreeWidgetItem(folderItem); + childItem->setText(0, item); + childItem->setFlags(childItem->flags() | Qt::ItemIsSelectable); + + if (item == current) { + childItem->setSelected(true); + folderItem->setExpanded(true); + } + } + } + } + + confirm_btn = new QPushButton(tr("Select")); + confirm_btn->setObjectName("confirm_btn"); + confirm_btn->setEnabled(false); + + QObject::connect(treeWidget, &QTreeWidget::itemSelectionChanged, [=]() { + QList selectedItems = treeWidget->selectedItems(); + if (!selectedItems.isEmpty()) { + selection = selectedItems.first()->text(0); + confirm_btn->setEnabled(selection != current); + } + }); + + ScrollView *scroll_view = new ScrollView(treeWidget, this); + scroll_view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + + main_layout->addWidget(scroll_view); + main_layout->addSpacing(35); + + // cancel + confirm buttons + QHBoxLayout *blayout = new QHBoxLayout; + main_layout->addLayout(blayout); + blayout->setSpacing(50); + + QPushButton *cancel_btn = new QPushButton(tr("Cancel")); + QObject::connect(cancel_btn, &QPushButton::clicked, this, &ConfirmationDialog::reject); + QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept); + blayout->addWidget(cancel_btn); + blayout->addWidget(confirm_btn); + + QVBoxLayout *outer_layout = new QVBoxLayout(this); + outer_layout->setContentsMargins(50, 50, 50, 50); + outer_layout->addWidget(container); +} + +QString TreeOptionDialog::getSelection(const QString &prompt_text, const QList> &items, + const QString ¤t, QWidget *parent) { + TreeOptionDialog d(prompt_text, items, current, parent); + if (d.exec()) { + return d.selection; + } + return ""; +} diff --git a/selfdrive/ui/qt/widgets/input.h b/selfdrive/ui/qt/widgets/input.h index 089e54e4a0..76f87bf32f 100644 --- a/selfdrive/ui/qt/widgets/input.h +++ b/selfdrive/ui/qt/widgets/input.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "selfdrive/ui/qt/widgets/keyboard.h" @@ -69,3 +70,16 @@ public: static QString getSelection(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent); QString selection; }; + +class TreeOptionDialog : public DialogBase { + Q_OBJECT + +public: + explicit TreeOptionDialog(const QString &prompt_text, const QList> &items, const QString ¤t, QWidget *parent = nullptr); + static QString getSelection(const QString &prompt_text, const QList> &items, const QString ¤t, QWidget *parent = nullptr); + QString selection; + +private: + QTreeWidget *treeWidget; + QPushButton *confirm_btn; +}; 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/soundd.py b/selfdrive/ui/soundd.py index facb398339..a94456efe0 100644 --- a/selfdrive/ui/soundd.py +++ b/selfdrive/ui/soundd.py @@ -139,7 +139,7 @@ class Soundd(QuietMode): # sounddevice must be imported after forking processes import sounddevice as sd - sm = messaging.SubMaster(['selfdriveState', 'microphone']) + sm = messaging.SubMaster(['selfdriveState', 'soundPressure']) with self.get_stream(sd) as stream: rk = Ratekeeper(20) @@ -150,8 +150,8 @@ class Soundd(QuietMode): self.load_param() - if sm.updated['microphone'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert - self.spl_filter_weighted.update(sm["microphone"].soundPressureWeightedDb) + if sm.updated['soundPressure'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert + self.spl_filter_weighted.update(sm["soundPressure"].soundPressureWeightedDb) self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x)) self.get_audible_alert(sm) diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 41cc09141b..810338aae8 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -21,6 +21,7 @@ qt_src = [ "sunnypilot/qt/home.cc", "sunnypilot/qt/offroad/exit_offroad_button.cc", "sunnypilot/qt/offroad/offroad_home.cc", + "sunnypilot/qt/offroad/settings/developer_panel.cc", "sunnypilot/qt/offroad/settings/device_panel.cc", "sunnypilot/qt/offroad/settings/lateral_panel.cc", "sunnypilot/qt/offroad/settings/longitudinal_panel.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc new file mode 100644 index 0000000000..5c9f0ff99e --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc @@ -0,0 +1,80 @@ +/** + * 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. + */ +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h" + +DeveloperPanelSP::DeveloperPanelSP(SettingsWindow *parent) : DeveloperPanel(parent) { + + // Advanced Controls Toggle + showAdvancedControls = new ParamControlSP("ShowAdvancedControls", tr("Show Advanced Controls"), tr("Toggle visibility of advanced sunnypilot controls.\nThis only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state."), ""); + addItem(showAdvancedControls); + + QObject::connect(showAdvancedControls, &ParamControlSP::toggleFlipped, this, [=](bool) { + AbstractControlSP::UpdateAllAdvancedControls(); + updateToggles(!uiState()->scene.started); + }); + showAdvancedControls->showDescription(); + + // Github Runner Toggle + enableGithubRunner = new ParamControlSP("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), "", this, true); + addItem(enableGithubRunner); + + // Quickboot Mode Toggle + prebuiltToggle = new ParamControlSP("QuickBootToggle", tr("Enable Quickboot Mode"), tr(""), "", this, true); + addItem(prebuiltToggle); + + QObject::connect(prebuiltToggle, &ParamControl::toggleFlipped, [=](bool state) { + QString prebuiltPath = "/data/openpilot/prebuilt"; + state ? QFile(prebuiltPath).open(QIODevice::WriteOnly) : QFile::remove(prebuiltPath); + prebuiltToggle->refresh(); + }); + prebuiltToggle->setVisible(false); + + // Error log button + errorLogBtn = new ButtonControlSP(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes.")); + connect(errorLogBtn, &ButtonControlSP::clicked, [=]() { + QFileInfo file("/data/community/crashes/error.log"); + QString text; + if (file.exists()) { + text = "" + file.lastModified().toString("dd-MMM-yyyy hh:mm:ss ").toUpper() + "

"; + } + text += QString::fromStdString(util::read_file("/data/community/crashes/error.log")); + ConfirmationDialog::rich(text, this); + }); + addItem(errorLogBtn); + + QObject::connect(uiState(), &UIState::offroadTransition, this, &DeveloperPanelSP::updateToggles); +} + +void DeveloperPanelSP::updateToggles(bool offroad) { + bool is_release = params.getBool("IsReleaseBranch"); + bool is_tested = params.getBool("IsTestedBranch"); + bool is_development = params.getBool("IsDevelopmentBranch"); + bool disable_updates = params.getBool("DisableUpdates"); + + prebuiltToggle->setVisible(!is_release && !is_tested && !is_development); + prebuiltToggle->setEnabled(disable_updates); + + params.putBool("QuickBootToggle", QFile::exists("/data/openpilot/prebuilt")); + prebuiltToggle->refresh(); + + prebuiltToggle->setDescription(disable_updates + ? tr("When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, " + "it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. " + "

To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.") + : tr("Quickboot mode requires updates to be disabled.
Enable 'Disable Updates' in the Software panel first.")); + + enableGithubRunner->setVisible(!is_release); + errorLogBtn->setVisible(!is_release); + showAdvancedControls->setEnabled(true); +} + +void DeveloperPanelSP::showEvent(QShowEvent *event) { + DeveloperPanel::showEvent(event); + updateToggles(!uiState()->scene.started); + AbstractControlSP::UpdateAllAdvancedControls(); + prebuiltToggle->showDescription(); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h new file mode 100644 index 0000000000..d63fe18d04 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h @@ -0,0 +1,30 @@ +/** + * 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. + */ +#pragma once +#include +#include + +#include "selfdrive/ui/qt/offroad/developer_panel.h" + +class DeveloperPanelSP : public DeveloperPanel { + Q_OBJECT +public: + explicit DeveloperPanelSP(SettingsWindow *parent); + +private: + ParamControlSP *enableGithubRunner; + ButtonControlSP *errorLogBtn; + ParamControlSP *prebuiltToggle; + Params params; + ParamControlSP *showAdvancedControls; + +private slots: + void updateToggles(bool offroad); + +protected: + void showEvent(QShowEvent *event) override; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index b89116d424..7e3d18bd81 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -87,6 +87,17 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { params.put("DeviceBootMode", QString::number(index).toStdString()); updateState(); }); + + interactivityTimeout = new OptionControlSP("InteractivityTimeout", tr("Interactivity Timeout"), + tr("Apply a custom timeout for settings UI." + "\nThis is the time after which settings UI closes automatically if user is not interacting with the screen."), + "", {0, 120}, 10, true, nullptr, false); + + connect(interactivityTimeout, &OptionControlSP::updateLabels, [=]() { + updateState(); + }); + + addItem(interactivityTimeout); // Brightness brightness = new Brightness(); @@ -198,4 +209,11 @@ void DevicePanelSP::updateState() { currStatus = DeviceSleepModeStatus::OFFROAD; } toggleDeviceBootMode->setDescription(deviceSleepModeDescription(currStatus)); + + QString timeoutValue = QString::fromStdString(params.get("InteractivityTimeout")); + if (timeoutValue == "0" || timeoutValue.isEmpty()) { + interactivityTimeout->setLabel("Default"); + } else { + interactivityTimeout->setLabel(timeoutValue + "s"); + } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h index 2cd3281d1f..899a942dae 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h @@ -33,6 +33,7 @@ private: MaxTimeOffroad *maxTimeOffroad; ButtonParamControlSP *toggleDeviceBootMode; Brightness *brightness; + OptionControlSP *interactivityTimeout; const QString alwaysOffroadStyle = R"( PushButtonSP { diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc index b8237c3627..dfdf79938a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include "common/model.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h" @@ -66,6 +68,19 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { connect(uiStateSP(), &UIStateSP::uiUpdate, this, &ModelsPanel::updateLabels); list->addItem(currentModelLblBtn); + refreshAvailableModelsBtn = new ButtonControlSP(tr("Refresh Model List"), tr("REFRESH"), "", this); + connect(refreshAvailableModelsBtn, &ButtonControlSP::clicked, this, [=]() { + params.put("ModelManager_LastSyncTime", "0"); + ConfirmationDialog::alert(tr("Fetching Latest Models"), this); + }); + + list->addItem(refreshAvailableModelsBtn); + + clearModelCacheBtn = new ButtonControlSP(tr("Clear Model Cache"), tr("CLEAR"), "", this); + connect(clearModelCacheBtn, &ButtonControlSP::clicked, this, &ModelsPanel::clearModelCache); + + list->addItem(clearModelCacheBtn); + // Create progress bars for downloads supercomboProgressBar = createProgressBar(this); QString supercomboType = tr("Driving Model"); @@ -90,11 +105,25 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { list->addItem(horizontal_line()); // LiveDelay toggle - list->addItem(new ParamControlSP("LagdToggle", - tr("Live Learning Steer Delay"), - 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."), - "../assets/offroad/icon_shell.png")); + lagd_toggle_control = new ParamControlSP("LagdToggle", tr("Live Learning Steer Delay"), "", "../assets/offroad/icon_shell.png"); + lagd_toggle_control->showDescription(); + list->addItem(lagd_toggle_control); + + // Software delay control + 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(); + delay_control->setLabel(QString::number(value, 'f', 2) + "s"); + }); + connect(lagd_toggle_control, &ParamControlSP::toggleFlipped, [=](bool state) { + delay_control->setVisible(!state); + }); + delay_control->showDescription(); + list->addItem(delay_control); } QProgressBar* ModelsPanel::createProgressBar(QWidget *parent) { @@ -230,28 +259,73 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() { currentModelLblBtn->setEnabled(false); currentModelLblBtn->setValue(tr("Fetching models...")); - // Create mapping of bundle indices to display names - QMap index_to_bundle; + struct ModelEntry { + QString folder; + QString displayName; + int index; + }; + QList sortedModels; + QSet modelFolders; const auto bundles = model_manager.getAvailableBundles(); - for (const auto &bundle: bundles) { - index_to_bundle.insert(bundle.getIndex(), QString::fromStdString(bundle.getDisplayName())); + + for (const auto &bundle : bundles) { + auto overrides = bundle.getOverrides(); + QString gen; + for (const auto &override : overrides) { + if (override.getKey() == "folder") { + gen = QString::fromStdString(override.getValue().cStr()); + } + } + + modelFolders.insert(gen); + sortedModels.append(ModelEntry{ + gen, + QString::fromStdString(bundle.getDisplayName()), + static_cast(bundle.getIndex()) + }); } - // Sort bundles by index in descending order - QStringList bundleNames; - // Add "Default" as the first option - bundleNames.append(DEFAULT_MODEL); + std::sort(sortedModels.begin(), sortedModels.end(), + [](const ModelEntry &a, const ModelEntry &b) { + return a.index > b.index; + }); - auto indices = index_to_bundle.keys(); - std::sort(indices.begin(), indices.end(), std::greater()); - for (const auto &index: indices) { - bundleNames.append(index_to_bundle[index]); + // Create a list of folder-maxIndex pairs for sorting + QList> folderMaxIndices; + for (const auto &folder : modelFolders) { + int maxIndex = -1; + for (const auto &model : sortedModels) { + if (model.folder == folder) { + maxIndex = std::max(maxIndex, model.index); + } + } + folderMaxIndices.append(qMakePair(folder, maxIndex)); } + // Sort folders by their highest model index + std::sort(folderMaxIndices.begin(), folderMaxIndices.end(), + [](const QPair &a, const QPair &b) { + return a.second > b.second; + }); + + // Create the final items list using sorted folders + QList> items; + for (const auto &folderPair : folderMaxIndices) { + QStringList folderModels; + for (const auto &model : sortedModels) { + if (model.folder == folderPair.first) { + folderModels.append(model.displayName); + } + } + items.append(qMakePair(folderPair.first, folderModels)); + } + + items.insert(0, qMakePair(QString(""), QStringList{DEFAULT_MODEL})); + currentModelLblBtn->setValue(GetActiveModelInternalName()); - const QString selectedBundleName = MultiOptionDialog::getSelection( - tr("Select a Model"), bundleNames, GetActiveModelName(), this); + const QString selectedBundleName = TreeOptionDialog::getSelection( + tr("Select a Model"), items, GetActiveModelName(), this); if (selectedBundleName.isEmpty() || !canContinueOnMeteredDialog()) { return; @@ -290,6 +364,20 @@ void ModelsPanel::updateLabels() { handleBundleDownloadProgress(); currentModelLblBtn->setEnabled(!is_onroad && !isDownloading()); currentModelLblBtn->setValue(GetActiveModelInternalName()); + + // Update lagdToggle description with current value + 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."); + lagd_toggle_control->setDescription(desc); + + delay_control->setVisible(!params.getBool("LagdToggle")); + if (delay_control->isVisible()) { + float value = QString::fromStdString(params.get("LagdToggleDelay")).toFloat(); + delay_control->setLabel(QString::number(value, 'f', 2) + "s"); + } + + clearModelCacheBtn->setValue(QString::number(calculateCacheSize(), 'f', 2) + " MB"); } /** @@ -310,3 +398,39 @@ void ModelsPanel::showResetParamsDialog() { params.remove("LiveTorqueParameters"); } } + +void ModelsPanel::clearModelCache() { + QString confirmMsg = tr("This will delete ALL downloaded models from the cache" + "
except the currently active model." + "

Are you sure you want to continue?"); + QString content("

" + tr("Driving Model Selector") + "


" + "

" + confirmMsg + "

"); + if (showConfirmationDialog( + content, + tr("Clear Cache"))) { + params.putBool("ModelManager_ClearCache", true); + } +} + +double ModelsPanel::calculateCacheSize() { + QFuture future_ModelCacheSize = QtConcurrent::run([=]() { + + QDir model_dir(QString::fromStdString(Path::model_root())); + QFileInfoList model_files = model_dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + qint64 totalSize = 0; + for (const QFileInfo &model_file : model_files) { + if (model_file.isFile()) { + totalSize += model_file.size(); + } + } + return totalSize; + }); + return static_cast(future_ModelCacheSize) / (1024.0 * 1024.0); +} + +void ModelsPanel::showEvent(QShowEvent *event) { + lagd_toggle_control->showDescription(); + if (delay_control->isVisible()) { + delay_control->showDescription(); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h index 8b8cf69759..8586d862bd 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h @@ -21,6 +21,7 @@ private: QString GetActiveModelName(); QString GetActiveModelInternalName(); void updateModelManagerState(); + void showEvent(QShowEvent *event) override; bool isDownloading() const { if (!model_manager.hasSelectedBundle()) { @@ -41,6 +42,8 @@ private: cereal::ModelManagerSP::Reader model_manager; cereal::ModelManagerSP::DownloadStatus download_status{}; cereal::ModelManagerSP::DownloadStatus prev_download_status{}; + void clearModelCache(); + double calculateCacheSize(); bool canContinueOnMeteredDialog() { if (!is_metered) return true; @@ -64,6 +67,8 @@ private: bool is_onroad = false; ButtonControlSP *currentModelLblBtn; + ParamControlSP *lagd_toggle_control; + OptionControlSP *delay_control; QProgressBar *supercomboProgressBar; QFrame *supercomboFrame; QProgressBar *navigationProgressBar; @@ -73,5 +78,7 @@ private: QProgressBar *policyProgressBar; QFrame *policyFrame; Params params; + ButtonControlSP *clearModelCacheBtn; + ButtonControlSP *refreshAvailableModelsBtn; }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index 05ea5ff899..5415e75ea9 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -8,10 +8,10 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" -#include "selfdrive/ui/qt/offroad/developer_panel.h" #include "selfdrive/ui/qt/offroad/firehose.h" #include "selfdrive/ui/sunnypilot/qt/network/networking.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" @@ -91,7 +91,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), PanelInfo(" " + tr("Vehicle"), new VehiclePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"), PanelInfo(" " + tr("Firehose"), new FirehosePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_firehose.svg"), - PanelInfo(" " + tr("Developer"), new DeveloperPanel(this), "../assets/icons/shell.png"), + PanelInfo(" " + tr("Developer"), new DeveloperPanelSP(this), "../assets/icons/shell.png"), }; nav_btns = new QButtonGroup(this); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc index 9430d02ba8..a1961cb1ea 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc @@ -18,6 +18,18 @@ SoftwarePanelSP::SoftwarePanelSP(QWidget *parent) : SoftwarePanel(parent) { searchBranches(d.text()); } }); + + // Disable Updates toggle + disableUpdatesToggle = new ParamControl("DisableUpdates", + tr("Disable Updates"), + tr("When enabled, software updates will be disabled. This requires a reboot to take effect."), + "../assets/icons/icon_warning.png", + this, true); + disableUpdatesToggle->showDescription(); + addItem(disableUpdatesToggle); + connect(disableUpdatesToggle, &ParamControl::toggleFlipped, this, &SoftwarePanelSP::handleDisableUpdatesToggled); + connect(uiState(), &UIState::offroadTransition, this, &SoftwarePanelSP::updateDisableUpdatesToggle); + updateDisableUpdatesToggle(!uiState()->scene.started); } /** @@ -49,3 +61,27 @@ void SoftwarePanelSP::searchBranches(const QString &query) { checkForUpdates(); } } + +void SoftwarePanelSP::handleDisableUpdatesToggled(bool state) { + if (ConfirmationDialog::confirm(tr("%1 updates requires a reboot.
Reboot now?") + .arg(state ? "Disabling" : "Enabling"), tr("Reboot"), this)) { + params.putBool("DoReboot", true); + } else { + params.putBool("DisableUpdates", !state); + disableUpdatesToggle->refresh(); + } +} + +void SoftwarePanelSP::updateDisableUpdatesToggle(bool offroad) { + bool enabled = offroad; + disableUpdatesToggle->setEnabled(enabled); + disableUpdatesToggle->setDescription(enabled + ? tr("When enabled, software updates will be disabled.
This requires a reboot to take effect.") + : tr("Please enable always offroad mode or turn off vehicle to adjust these toggles")); +} + +void SoftwarePanelSP::showEvent(QShowEvent *event) { + SoftwarePanel::showEvent(event); + updateDisableUpdatesToggle(!uiState()->scene.started); + disableUpdatesToggle->showDescription(); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h index 56d8b9be5b..efaa836a21 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h @@ -19,4 +19,10 @@ public: private: void searchBranches(const QString &query); + ParamControl *disableUpdatesToggle = nullptr; + void handleDisableUpdatesToggled(bool state); +private slots: + void updateDisableUpdatesToggle(bool offroad); +protected: + void showEvent(QShowEvent *event) override; }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc index 1358cbc959..3d4e070963 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc @@ -66,6 +66,14 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { }); list->addItem(horizontal_line()); + QString sunnylinkUploaderDesc = tr("Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)"); + sunnylinkUploaderEnabledBtn = new ParamControlSP( + "EnableSunnylinkUploader", + tr("[Don't use] Enable sunnylink uploader"), + sunnylinkUploaderDesc, + "", nullptr, true); + list->addItem(sunnylinkUploaderEnabledBtn); + connect(sunnylinkEnabledBtn, &ParamControl::showDescriptionEvent, [=]() { // resets the description to the default one for the Easter egg sunnylinkEnabledBtn->setDescription(sunnylinkEnabledBtnDesc); @@ -261,6 +269,8 @@ void SunnylinkPanel::updatePanel() { pairSponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled); pairSponsorBtn->setValue(is_paired ? tr("Paired") : tr("Not Paired")); + sunnylinkUploaderEnabledBtn->setEnabled(max_current_sponsor_rule.roleTier == SponsorTier::Guardian && is_sunnylink_enabled); + if (!is_sunnylink_enabled) { sunnylinkEnabledBtn->setValue(""); sponsorBtn->setValue(""); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h index e17c68d3a3..c56d8ebd1a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h @@ -49,6 +49,7 @@ private: QString sunnylinkBtnDescription; PushButtonSP *restoreSettings; PushButtonSP *backupSettings; + ParamControlSP * sunnylinkUploaderEnabledBtn; void stopSunnylink() const; void startSunnylink() const; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc index 0d42bd74f4..818da9b75e 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc @@ -12,7 +12,7 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) { connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { paramsRefresh(); }); - + main_layout = new QStackedLayout(this); ListWidgetSP *list = new ListWidgetSP(this, false); @@ -30,6 +30,7 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) { }, }; + // Add regular toggles first for (auto &[param, title, desc, icon, needs_restart] : toggle_defs) { auto toggle = new ParamControlSP(param, title, desc, icon, this); @@ -53,9 +54,20 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) { param_watcher->addParam(param); } + // Visuals: Display Metrics below Chevron + std::vector chevron_info_settings_texts{tr("Off"), tr("Distance"), tr("Speed"), tr("Time"), tr("All")}; + chevron_info_settings = new ButtonParamControlSP( + "ChevronInfo", tr("Display Metrics Below Chevron"), tr("Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control)."), + "", + chevron_info_settings_texts, + 200); + chevron_info_settings->showDescription(); + list->addItem(chevron_info_settings); + param_watcher->addParam("ChevronInfo"); + sunnypilotScroller = new ScrollViewSP(list, this); vlayout->addWidget(sunnypilotScroller); - + main_layout->addWidget(sunnypilotScreen); } @@ -67,4 +79,8 @@ void VisualsPanel::paramsRefresh() { for (auto toggle : toggles) { toggle.second->refresh(); } + + if (chevron_info_settings) { + chevron_info_settings->refresh(); + } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h index 42e0688957..f342662c22 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h @@ -27,4 +27,5 @@ protected: Params params; std::map toggles; ParamWatcher * param_watcher; + ButtonParamControlSP *chevron_info_settings; }; diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc index d873913b57..28374f3a9f 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc @@ -30,9 +30,24 @@ QFrame *vertical_space(int height, QWidget *parent) { } // AbstractControlSP +std::vector AbstractControlSP::advanced_controls_; +AbstractControlSP::~AbstractControlSP() { UnregisterAdvancedControl(this); } -AbstractControlSP::AbstractControlSP(const QString &title, const QString &desc, const QString &icon, QWidget *parent) - : AbstractControl(title, desc, icon, parent) { +void AbstractControlSP::RegisterAdvancedControl(AbstractControlSP *ctrl) { advanced_controls_.push_back(ctrl); } + +void AbstractControlSP::UnregisterAdvancedControl(AbstractControlSP *ctrl) { + advanced_controls_.erase(std::remove(advanced_controls_.begin(), advanced_controls_.end(), ctrl), advanced_controls_.end()); +} + +void AbstractControlSP::UpdateAllAdvancedControls() { + bool visibility = Params().getBool("ShowAdvancedControls"); + advanced_controls_.erase(std::remove(advanced_controls_.begin(), advanced_controls_.end(), nullptr), advanced_controls_.end()); + for (auto *ctrl : advanced_controls_) ctrl->setVisible(visibility); +} + +AbstractControlSP::AbstractControlSP(const QString &title, const QString &desc, const QString &icon, QWidget *parent, bool advancedControl) + : AbstractControl(title, desc, icon, parent), isAdvancedControl(advancedControl) { + if (isAdvancedControl) RegisterAdvancedControl(this); main_layout = new QVBoxLayout(this); main_layout->setMargin(0); @@ -82,8 +97,8 @@ void AbstractControlSP::hideEvent(QHideEvent *e) { } } -AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, const QString &desc, const QString &icon, QWidget *parent) - : AbstractControlSP(title, desc, icon, parent) { +AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, const QString &desc, const QString &icon, QWidget *parent, bool advancedControl) + : AbstractControlSP(title, desc, icon, parent, advancedControl) { if (title_label != nullptr) { delete title_label; @@ -169,8 +184,8 @@ void AbstractControlSP_SELECTOR::hideEvent(QHideEvent *e) { // controls -ButtonControlSP::ButtonControlSP(const QString &title, const QString &text, const QString &desc, QWidget *parent) - : AbstractControlSP(title, desc, "", parent) { +ButtonControlSP::ButtonControlSP(const QString &title, const QString &text, const QString &desc, QWidget *parent, bool advancedControl) + : AbstractControlSP(title, desc, "", parent, advancedControl) { btn.setText(text); btn.setStyleSheet(R"( @@ -225,8 +240,8 @@ void ElidedLabelSP::paintEvent(QPaintEvent *event) { // ParamControlSP -ParamControlSP::ParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent) - : ToggleControlSP(title, desc, icon, false, parent) { +ParamControlSP::ParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent, bool advancedControl) + : ToggleControlSP(title, desc, icon, false, parent, advancedControl){ key = param.toStdString(); QObject::connect(this, &ParamControlSP::toggleFlipped, this, &ParamControlSP::toggleClicked); diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index c8849faf53..a840febfe7 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -57,6 +57,7 @@ class AbstractControlSP : public AbstractControl { Q_OBJECT public: + ~AbstractControlSP(); void setDescription(const QString &desc) override { if (description) description->setText(desc); } @@ -81,13 +82,30 @@ public slots: description->setVisible(true); } + void setVisible(bool visible) override { + bool _visible = visible; + if (isAdvancedControl && !params.getBool("ShowAdvancedControls")) { + _visible = false; + } + AbstractControl::setVisible(_visible); + } + + static void RegisterAdvancedControl(AbstractControlSP *ctrl); + static void UnregisterAdvancedControl(AbstractControlSP *ctrl); + static void UpdateAllAdvancedControls(); + protected: - AbstractControlSP(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); + AbstractControlSP(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr, bool advancedControl = false); void hideEvent(QHideEvent *e) override; QVBoxLayout *main_layout; ElidedLabelSP *value; QLabel *description = nullptr; + bool isAdvancedControl; + +private: + Params params; + static std::vector advanced_controls_; }; // AbstractControlSP_SELECTOR @@ -97,7 +115,7 @@ class AbstractControlSP_SELECTOR : public AbstractControlSP { protected: QSpacerItem *spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); - AbstractControlSP_SELECTOR(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); + AbstractControlSP_SELECTOR(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr, bool advancedControl = false); void hideEvent(QHideEvent *e) override; }; @@ -123,7 +141,7 @@ class ButtonControlSP : public AbstractControlSP { Q_OBJECT public: - ButtonControlSP(const QString &title, const QString &text, const QString &desc = "", QWidget *parent = nullptr); + ButtonControlSP(const QString &title, const QString &text, const QString &desc = "", QWidget *parent = nullptr, bool advancedControl = false); inline void setText(const QString &text) { btn.setText(text); } inline QString text() const { return btn.text(); } inline void click() { btn.click(); } @@ -142,7 +160,7 @@ class ToggleControlSP : public AbstractControlSP { Q_OBJECT public: - ToggleControlSP(const QString &title, const QString &desc = "", const QString &icon = "", const bool state = false, QWidget *parent = nullptr) : AbstractControlSP(title, desc, icon, parent) { + ToggleControlSP(const QString &title, const QString &desc = "", const QString &icon = "", const bool state = false, QWidget *parent = nullptr, bool advancedControl = false) : AbstractControlSP(title, desc, icon, parent, advancedControl) { // space between toggle and title icon_label = new QLabel(this); hlayout->addWidget(icon_label); @@ -173,7 +191,7 @@ class ParamControlSP : public ToggleControlSP { Q_OBJECT public: - ParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr); + ParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr, bool advancedControl = false); void setConfirmation(bool _confirm, bool _store_confirm) { confirm = _confirm; store_confirm = _store_confirm; @@ -219,7 +237,7 @@ class MultiButtonControlSP : public AbstractControlSP_SELECTOR { public: MultiButtonControlSP(const QString &title, const QString &desc, const QString &icon, - const std::vector &button_texts, const int minimum_button_width = 225, const bool inline_layout = false) : AbstractControlSP_SELECTOR(title, desc, icon), button_texts(button_texts), is_inline_layout(inline_layout) { + const std::vector &button_texts, const int minimum_button_width = 225, const bool inline_layout = false, bool advancedControl = false) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr, advancedControl), button_texts(button_texts), is_inline_layout(inline_layout) { const QString style = R"( QPushButton { border-radius: 20px; @@ -371,8 +389,8 @@ class ButtonParamControlSP : public MultiButtonControlSP { Q_OBJECT public: ButtonParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, - const std::vector &button_texts, const int minimum_button_width = 225, const bool inline_layout = false) : MultiButtonControlSP(title, desc, icon, - button_texts, minimum_button_width, inline_layout) { + const std::vector &button_texts, const int minimum_button_width = 225, const bool inline_layout = false, bool advancedControl = false) : MultiButtonControlSP(title, desc, icon, + button_texts, minimum_button_width, inline_layout, advancedControl) { key = param.toStdString(); int value = atoi(params.get(key).c_str()); @@ -480,6 +498,16 @@ private: return result.toInt(); } + int getParamValueScaled() { + const auto param_value = QString::fromStdString(params.get(key)); + return static_cast(param_value.toFloat() * 100); + } + + void setParamValueScaled(const int new_value) { + const float scaled_value = new_value / 100.0f; + params.put(key, QString::number(scaled_value, 'f', 2).toStdString()); + } + // Although the method is not static, and thus has access to the value property, I prefer to be explicit about the value. void setParamValue(const int new_value) { const auto value_str = valueMap != nullptr ? valueMap->value(QString::number(new_value)) : QString::number(new_value); @@ -488,7 +516,8 @@ private: public: OptionControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, - const MinMaxValue &range, const int per_value_change = 1, const bool inline_layout = false, const QMap *valMap = nullptr) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr), _title(title), valueMap(valMap), is_inline_layout(inline_layout) { + const MinMaxValue &range, const int per_value_change = 1, const bool inline_layout = false, + const QMap *valMap = nullptr, bool scale_float = false, bool advancedControl = false) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr, advancedControl), _title(title), valueMap(valMap), is_inline_layout(inline_layout), use_float_scaling(scale_float) { const QString style = R"( QPushButton { border-radius: 20px; @@ -528,7 +557,7 @@ public: const std::vector button_texts{"-", "+"}; key = param.toStdString(); - value = getParamValue(); + value = use_float_scaling ? getParamValueScaled() : getParamValue(); button_group = new QButtonGroup(this); button_group->setExclusive(true); @@ -546,10 +575,15 @@ public: QObject::connect(button, &QPushButton::clicked, [=]() { int change_value = (i == 0) ? -per_value_change : per_value_change; - value = getParamValue(); // in case it changed externally, we need to get the latest value. + value = use_float_scaling ? getParamValueScaled() : getParamValue(); value += change_value; value = std::clamp(value, range.min_value, range.max_value); - setParamValue(value); + + if (use_float_scaling) { + setParamValueScaled(value); + } else { + setParamValue(value); + } button_group->button(0)->setEnabled(!(value <= range.min_value)); button_group->button(1)->setEnabled(!(value >= range.max_value)); @@ -642,6 +676,7 @@ private: const QString label_disabled_style = "font-size: 50px; font-weight: 450; color: #5C5C5C;"; bool button_enabled = true; + bool use_float_scaling = false; }; class PushButtonSP : public QPushButton { 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 de24097d8f..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 إعادة التشغيل والتحديث @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,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 السابق @@ -160,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 @@ -171,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 @@ -179,6 +202,22 @@ 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 لمزيد من المعلومات. + + 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. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - 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. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW مراجعة - - Review the rules, features, and limitations of sunnypilot - مراجعة الأدوار والميزات والقيود في sunnypilot - Are you sure you want to review the training guide? هل أنت متأكد أنك تريد مراجعة دليل التدريب؟ @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off إيقاف التشغيل - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - يحتاج sunnypilot أن يتم ضبط الجهاز ضمن حدود 4 درجات يميناً أو يساراً و5 درجات نحو الأعلى أو 9 نحو الأسفل. يقوم sunnypilot بالمعايرة باستمرار، ونادراً ما يحتاج إلى عملية إعادة الضبط. - Your device is pointed %1° %2 and %3° %4. يشير جهازك إلى %1 درجة %2، و%3 درجة %4. @@ -359,12 +390,44 @@ Please use caution when using this feature. Only use the blinker when traffic an - Resetting calibration will restart openpilot if the car is powered on. + 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. + + + + + +Steering lag calibration is complete. + + + + Steering torque response calibration is %1% complete. + + + + 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. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -381,6 +444,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? هل أنت متأكد أنك تريد مراجعة دليل التدريب؟ @@ -393,6 +460,19 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 إعادة التشغيل @@ -411,7 +491,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - تأكيد + Are you sure you want to enter Always Offroad mode? @@ -421,22 +501,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. @@ -449,6 +513,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + ⁍ Default: Device will boot/wake-up normally & will be ready to engage. + + + + ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +568,21 @@ Please use caution when using this feature. Only use the blinker when traffic an بدء تشغيل الكاميرا + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,10 +596,6 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - 🔥 وضع خرطوم الحريق 🔥 - Firehose Mode: ACTIVE وضع خرطوم الحريق: نشط @@ -509,10 +604,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. @@ -524,6 +615,14 @@ Please use caution when using this feature. Only use the blinker when traffic an حتى الآن، يوجد </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. @@ -531,7 +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 + 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. @@ -644,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). @@ -661,6 +768,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -668,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. @@ -683,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. @@ -711,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. @@ -723,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. @@ -770,31 +900,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp اختيار - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -810,21 +953,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -833,6 +972,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. + + Model download has started in the background. @@ -853,6 +996,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -976,14 +1127,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 @@ -998,28 +1141,46 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 تغييراً في موقع تركيب الجهاز. تأكد من تثبيت الجهاز بشكل كامل في موقعه وتثبيته بإحكام على الزجاج الأمامي. + - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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. + +%1 @@ -1039,11 +1200,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - sunnypilot غير متوفر + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY تحكم على الفور @@ -1060,6 +1224,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive النظام لا يستجيب + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + التحقق + + + Country + + + + SELECT + اختيار + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + تحديث + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1146,7 +1445,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - تأكيد + Cancel @@ -1229,10 +1528,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1270,46 +1565,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 + @@ -1370,29 +1628,13 @@ This may take up to a minute. البرنامج - Trips + Models - - Vehicle - - - - Developer - المطور - - - Firehose - خرطوم الحريق - Steering - - Models - - Cruise @@ -1401,100 +1643,25 @@ This may take up to a minute. Visuals - - - Setup - WARNING: Low Voltage - تحذير: الجهد منخفض + OSM + - Power your device in a car with a harness or proceed at your own risk. - شغل جهازك في السيارة عن طريق شرطان التوصيل، أو تابع على مسؤوليتك. + Trips + - Power off - إيقاف التشغيل + Vehicle + - Continue - متابعة + Firehose + خرطوم الحريق - 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 - البرمجيات المخصصة + Developer + المطور @@ -1699,6 +1866,14 @@ This may take up to a minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1707,6 +1882,22 @@ This may take up to a minute. Select a branch اختر فرعاً + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + إعادة التشغيل + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1764,22 +1955,6 @@ This may take up to a minute. 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 @@ -1804,24 +1979,20 @@ This may take up to a minute. 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 🎉. @@ -1876,6 +2047,26 @@ This may take up to a minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -1928,11 +2119,11 @@ This may take up to a minute. 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> قبل الاستمرار. + @@ -1965,10 +2156,6 @@ This may take up to a minute. Disengage on Accelerator Pedal فك الارتباط عن دواسة الوقود - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - عند تمكين هذه الميزة، فإن الضغط على دواسة الوقود سيؤدي إلى فك ارتباط sunnypilot. - Experimental Mode الوضع التجريبي @@ -1989,18 +2176,10 @@ This may take up to a minute. 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 تصور القيادة الديد @@ -2010,16 +2189,8 @@ This may take up to a minute. الوضع التجريبي غير متوفر حالياً في هذه السيارة نظراً لاستخدام رصيد التحكم التكيفي بالسرعة من أجل التحكم الطولي. - 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. @@ -2030,8 +2201,34 @@ This may take up to a minute. مراقبة السائق المستمرة - 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 @@ -2042,11 +2239,53 @@ This may take up to a minute. - Enable sunnypilot - تمكين sunnypilot + When enabled, pressing the accelerator pedal will disengage 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 driver monitoring even when sunnypilot is not engaged. + + + + 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 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: + + + + 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. + + + + + TreeOptionDialog + + Select + اختيار + + + Cancel + إلغاء + + + + VisualsPanel + + Show Blind Spot Warnings + + + + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. @@ -2054,43 +2293,32 @@ This may take up to a minute. - openpilot longitudinal control may come in a future update. + Off - - - Updater - Update Required - التحديث مطلوب + Distance + - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - تحديث نظام التشغيل مطلوب. قم بوصل جهازك بشبكة واي فاي من أجل تحديث أسرع. حجم التحميل حوالي 1 غيغا بايت تقريباً. + Speed + - Connect to Wi-Fi - الاتصال بشبكة الواي فاي + Time + - Install - تثبيت + All + - Back - السابق + Display Metrics Below Chevron + - Loading... - يتم التحميل... - - - Reboot - إعادة التشغيل - - - Update failed - فشل التحديث + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2100,12 +2328,12 @@ This may take up to a minute. انفتح - <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 6229920121..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 @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -191,6 +206,18 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -199,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -211,6 +242,14 @@ Please use caution when using this feature. Only use the blinker when traffic an View the error log for sunnypilot crashes. + + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> + + + + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. + + DevicePanel @@ -350,16 +389,36 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. + + + + + +Steering lag calibration is complete. + + + + Steering torque response calibration is %1% complete. + + + + 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. sunnypilot is continuously calibrating, resetting is rarely required. - - - - Resetting calibration will restart openpilot if the car is powered on. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. @@ -401,6 +460,19 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 @@ -419,7 +491,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - Bestätigen + Are you sure you want to enter Always Offroad mode? @@ -449,6 +521,18 @@ Please use caution when using this feature. Only use the blinker when traffic an Always Offroad + + ⁍ Default: Device will boot/wake-up normally & will be ready to engage. + + + + ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +568,21 @@ Please use caution when using this feature. Only use the blinker when traffic an Kamera startet + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,10 +596,6 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - 🔥 Firehose-Modus 🔥 - Firehose Mode: ACTIVE Firehose-Modus: AKTIV @@ -520,6 +615,10 @@ Please use caution when using this feature. Only use the blinker when traffic an <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. @@ -636,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). @@ -653,6 +760,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -762,31 +892,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp AUSWÄHLEN - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -802,21 +945,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -825,6 +964,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. + + Model download has started in the background. @@ -845,6 +988,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -978,22 +1129,30 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 @@ -1011,7 +1170,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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. + +%1 @@ -1030,6 +1191,13 @@ Firehose Mode allows you to maximize your training data uploads to improve openp HINWEIS + + OffroadHomeSP + + ALWAYS OFFROAD ACTIVE + + + OnroadAlerts @@ -1053,6 +1221,137 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + ÜBERPRÜFEN + + + Country + + + + SELECT + AUSWÄHLEN + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + Aktualisieren + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + + PairingPopup @@ -1138,7 +1437,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - Bestätigen + Cancel @@ -1251,47 +1550,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - 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 @@ -1361,6 +1619,14 @@ Dies kann bis zu einer Minute dauern. Cruise + + Visuals + + + + OSM + + Trips @@ -1377,105 +1643,6 @@ Dies kann bis zu einer Minute dauern. Developer Entwickler - - Visuals - - - - - 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 - - SetupWidget @@ -1681,6 +1848,14 @@ Dies kann bis zu einer Minute dauern. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1689,6 +1864,22 @@ Dies kann bis zu einer Minute dauern. Select a branch Wähle einen Branch + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + Neustart + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1991,6 +2182,28 @@ Dies kann bis zu einer Minute dauern. 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 @@ -2019,10 +2232,6 @@ Dies kann bis zu einer Minute dauern. 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: @@ -2041,38 +2250,57 @@ Dies kann bis zu einer Minute dauern. - Updater + TreeOptionDialog - Update Required - Aktualisierung notwendig + Select + Auswählen - 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. + Cancel + Abbrechen + + + + VisualsPanel + + Show Blind Spot Warnings + - Connect to Wi-Fi - Mit WLAN verbinden + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. + - Install - Installieren + Changing this setting will restart openpilot if the car is powered on. + - Back - Zurück + Off + - Loading... - Laden... + Distance + - Reboot - Neustart + Speed + - Update failed - Aktualisierung fehlgeschlagen + Time + + + + All + + + + Display Metrics Below Chevron + + + + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 3d9622166a..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 @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,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 @@ -160,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 @@ -171,12 +186,36 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode Modo de maniobra longitudinal + + 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. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - 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. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -274,10 +313,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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? @@ -314,10 +349,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off Apagar - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilot requiere que el dispositivo sea montado entre 4° grados a la izquierda o derecha y entre 5° grados hacia arriba o 9° grados hacia abajo. sunnypilot está constantemente en calibración, formatear rara vez es necesario. - Your device is pointed %1° %2 and %3° %4. Su dispositivo está apuntando %1° %2 y %3° %4. @@ -356,15 +387,51 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 - Resetting calibration will restart openpilot if the car is powered on. + 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 @@ -381,6 +448,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? ¿Seguro que quiere revisar la guía de entrenamiento? @@ -393,6 +464,19 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 @@ -411,7 +495,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - Confirmar + Are you sure you want to enter Always Offroad mode? @@ -421,22 +505,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. @@ -449,6 +517,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + ⁍ Default: Device will boot/wake-up normally & will be ready to engage. + + + + ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +572,21 @@ Please use caution when using this feature. Only use the blinker when traffic an iniciando cámara + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,29 +600,29 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - - 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. @@ -527,7 +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 + 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. @@ -636,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). @@ -653,6 +764,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -660,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. @@ -675,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. @@ -703,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. @@ -715,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. @@ -762,31 +896,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECCIONAR - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -802,21 +949,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -825,6 +968,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. + + Model download has started in the background. @@ -845,6 +992,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -968,14 +1123,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 @@ -990,28 +1137,46 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. + - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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. + +%1 @@ -1031,11 +1196,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - sunnypilot no disponible + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY TOME CONTROL INMEDIATAMENTE @@ -1052,6 +1220,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive Systema no responde + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + VERIFICAR + + + Country + + + + SELECT + SELECCIONAR + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + ACTUALIZAR + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1138,7 +1441,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - Confirmar + Cancel @@ -1221,10 +1524,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - now ahora @@ -1250,46 +1549,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 + @@ -1320,7 +1582,7 @@ Esto puede tardar un minuto. Firehose - + Firehose @@ -1350,29 +1612,13 @@ Esto puede tardar un minuto. Software - Trips - - - - Vehicle - - - - Developer - Desarrollador - - - Firehose + Models Steering - - Models - - Cruise @@ -1381,100 +1627,25 @@ Esto puede tardar un minuto. Visuals - - - Setup - Something went wrong. Reboot the device. - Algo ha ido mal. Reinicie el dispositivo. + OSM + - 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. + Trips + - No custom software found at this URL. - No encontramos software personalizado en esta URL. + Vehicle + - WARNING: Low Voltage - ALERTA: Voltaje bajo + Firehose + Firehose - 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 + Developer + Desarrollador @@ -1679,6 +1850,14 @@ Esto puede tardar un minuto. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1687,6 +1866,22 @@ Esto puede tardar un minuto. Select a branch Selecione una rama + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + Reiniciar + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1744,37 +1939,21 @@ Esto puede tardar un minuto. 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 @@ -1782,27 +1961,23 @@ Esto puede tardar un minuto. 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 @@ -1856,44 +2031,64 @@ Esto puede tardar un minuto. 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 + @@ -1925,10 +2120,6 @@ Esto puede tardar un minuto. 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 @@ -1941,10 +2132,6 @@ Esto puede tardar un minuto. 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 @@ -1977,22 +2164,10 @@ Esto puede tardar un minuto. 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 @@ -2006,12 +2181,38 @@ Esto puede tardar un minuto. 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 @@ -2022,70 +2223,101 @@ Esto puede tardar un minuto. - 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. + + + + 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: + + + + 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. - Updater + TreeOptionDialog - Update Required - Actualización Requerida + Select + Seleccionar - 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. + Cancel + Cancelar + + + + VisualsPanel + + Show Blind Spot Warnings + - Connect to Wi-Fi - Conectarse a la Wi-Fi + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. + - Install - Instalar + Changing this setting will restart openpilot if the car is powered on. + Cambiar esta configuración reiniciará Openpilot si el automóvil está encendido. - Back - Volver + Off + - Loading... - Cargando... + Distance + - Reboot - Reiniciar + Speed + - Update failed - Actualización fallida + Time + + + + All + + + + Display Metrics Below Chevron + + + + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + 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 741cc606a7..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 @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,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 @@ -160,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 @@ -171,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 @@ -179,6 +202,22 @@ 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. + + 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. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - 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. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -262,10 +301,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 ? @@ -302,10 +337,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off Éteindre - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilot nécessite que l'appareil soit monté à 4° à gauche ou à droite et à 5° vers le haut ou 9° vers le bas. sunnypilot se calibre en continu, la réinitialisation est rarement nécessaire. - Your device is pointed %1° %2 and %3° %4. Votre appareil est orienté %1° %2 et %3° %4. @@ -359,12 +390,44 @@ Please use caution when using this feature. Only use the blinker when traffic an - Resetting calibration will restart openpilot if the car is powered on. + 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. + + + + + +Steering lag calibration is complete. + + + + Steering torque response calibration is %1% complete. + + + + 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. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -381,6 +444,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? Êtes-vous sûr de vouloir revoir le guide de formation ? @@ -393,6 +460,19 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 @@ -411,7 +491,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - Confirmer + Are you sure you want to enter Always Offroad mode? @@ -421,22 +501,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. @@ -449,6 +513,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + ⁍ Default: Device will boot/wake-up normally & will be ready to engage. + + + + ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +568,21 @@ Please use caution when using this feature. Only use the blinker when traffic an démarrage de la caméra + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,10 +596,6 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - - Firehose Mode: ACTIVE @@ -509,10 +604,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. @@ -520,6 +611,14 @@ Please use caution when using this feature. Only use the blinker when traffic an + + <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. @@ -527,7 +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 + 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. @@ -636,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). @@ -653,6 +760,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -660,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. @@ -675,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. @@ -703,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. @@ -715,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. @@ -762,31 +892,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SÉLECTIONNER - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -802,21 +945,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -825,6 +964,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. + + Model download has started in the background. @@ -845,6 +988,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -968,14 +1119,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 @@ -990,28 +1133,46 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. + - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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. + +%1 @@ -1031,11 +1192,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - sunnypilot indisponible + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY REPRENEZ LE CONTRÔLE IMMÉDIATEMENT @@ -1052,6 +1216,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive Système inopérant + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + VÉRIFIER + + + Country + + + + SELECT + SÉLECTIONNER + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + MISE À JOUR + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1138,7 +1437,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - Confirmer + Cancel @@ -1221,10 +1520,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1250,46 +1545,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 + @@ -1350,29 +1608,13 @@ Cela peut prendre jusqu'à une minute. Logiciel - Trips - - - - Vehicle - - - - Developer - Dév. - - - Firehose + Models Steering - - Models - - Cruise @@ -1381,100 +1623,25 @@ Cela peut prendre jusqu'à une minute. Visuals - - - Setup - Something went wrong. Reboot the device. - Un problème est survenu. Redémarrez l'appareil. + OSM + - 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. + Trips + - No custom software found at this URL. - Aucun logiciel personnalisé trouvé à cette URL. + Vehicle + - WARNING: Low Voltage - ATTENTION : Tension faible + Firehose + - 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é + Developer + Dév. @@ -1679,6 +1846,14 @@ Cela peut prendre jusqu'à une minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1687,6 +1862,22 @@ Cela peut prendre jusqu'à une minute. Select a branch Sélectionner une branche + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + Redémarrer + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1744,22 +1935,6 @@ Cela peut prendre jusqu'à une minute. 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 @@ -1784,24 +1959,20 @@ Cela peut prendre jusqu'à une minute. 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 🎉. @@ -1856,6 +2027,26 @@ Cela peut prendre jusqu'à une minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -1925,10 +2116,6 @@ Cela peut prendre jusqu'à une minute. 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 @@ -1969,14 +2156,6 @@ Cela peut prendre jusqu'à une minute. 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 @@ -1986,21 +2165,13 @@ Cela peut prendre jusqu'à une minute. 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. @@ -2010,8 +2181,34 @@ Cela peut prendre jusqu'à une minute. 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 @@ -2022,11 +2219,53 @@ Cela peut prendre jusqu'à une minute. - Enable sunnypilot - Activer sunnypilot + When enabled, pressing the accelerator pedal will disengage 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 driver monitoring even when sunnypilot is not engaged. + + + + 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 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: + + + + 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. + + + + + TreeOptionDialog + + Select + Sélectionner + + + Cancel + Annuler + + + + VisualsPanel + + Show Blind Spot Warnings + + + + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. @@ -2034,43 +2273,32 @@ Cela peut prendre jusqu'à une minute. - openpilot longitudinal control may come in a future update. + Off - - - Updater - Update Required - Mise à jour requise + Distance + - 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. + Speed + - Connect to Wi-Fi - Se connecter au Wi-Fi + Time + - Install - Installer + All + - Back - Retour + Display Metrics Below Chevron + - Loading... - Chargement... - - - Reboot - Redémarrer - - - Update failed - Échec de la mise à jour + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2080,11 +2308,11 @@ Cela peut prendre jusqu'à une minute. - <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 f85ee37689..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接続では大容量データをアップロードしません @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,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 戻る @@ -160,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 @@ -171,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を有効にする @@ -179,6 +202,22 @@ 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 を参照してください。 + + 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. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - 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. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW 確認 - - Review the rules, features, and limitations of sunnypilot - sunnypilotのルール、機能、および制限を確認してください - Are you sure you want to review the training guide? トレーニングガイドを始めてもよろしいですか? @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off パワーオフ - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilotの本体は左右4°以内、上5°下9°以内の角度で取付ける必要があります。常にキャリブレーションされておりリセットはほとんど必要ありません。 - Your device is pointed %1° %2 and %3° %4. このデバイスは%2 %1°、%4 %3°の向きに設置されています。 @@ -356,15 +387,51 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 - Resetting calibration will restart openpilot if the car is powered on. + 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 @@ -381,6 +448,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? トレーニングガイドを始めてもよろしいですか? @@ -393,6 +464,19 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 再起動 @@ -411,7 +495,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - 確認 + Are you sure you want to enter Always Offroad mode? @@ -421,22 +505,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. @@ -449,6 +517,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + ⁍ Default: Device will boot/wake-up normally & will be ready to engage. + + + + ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +572,21 @@ Please use caution when using this feature. Only use the blinker when traffic an カメラ起動中 + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,28 +600,28 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - 🔥 Firehoseモード 🔥 - 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. @@ -526,8 +629,8 @@ 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>: 大容量のネットワークに接続してください + 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. + @@ -634,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). @@ -651,6 +762,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -658,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. @@ -673,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. @@ -701,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. @@ -713,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. @@ -760,31 +894,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 選択 - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -800,21 +947,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -823,6 +966,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. + + Model download has started in the background. @@ -843,6 +990,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -962,14 +1117,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 @@ -984,32 +1131,50 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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)が検出されました。このドライブを使用するとデバイスが多大な電力を消費し過熱する可能性があります。 - - - 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 - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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 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. + + + + 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. + +%1 @@ -1029,11 +1194,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - sunnypilotは使用できません + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY 直ちに車の運転に戻って下さい @@ -1050,6 +1218,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive システムが応答しません + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + 確認 + + + Country + + + + SELECT + 選択 + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + 更新 + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1136,7 +1439,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - 確認 + Cancel @@ -1219,10 +1522,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1245,46 +1544,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 + @@ -1345,29 +1607,13 @@ This may take up to a minute. ソフト - Trips + Models - - Vehicle - - - - Developer - 開発 - - - Firehose - データ学習 - Steering - - Models - - Cruise @@ -1376,100 +1622,25 @@ This may take up to a minute. Visuals - - - Setup - WARNING: Low Voltage - 警告:電圧低下 + OSM + - Power your device in a car with a harness or proceed at your own risk. - ハーネスを使って車でデバイスに電源を供給するか、自己責任でこのまま継続して下さい。 + Trips + - Power off - 電源を切る + Vehicle + - Continue - 続ける + Firehose + データ学習 - 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 - カスタムソフトウェア + Developer + 開発 @@ -1674,6 +1845,14 @@ This may take up to a minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1682,6 +1861,22 @@ This may take up to a minute. Select a branch ブランチを選択 + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + 再起動 + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1691,7 +1886,7 @@ This may take up to a minute. 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 @@ -1699,7 +1894,7 @@ This may take up to a minute. Enter your GitHub username - GitHub のユーザー名を入力してください + GitHubのユーザー名を入力してください LOADING @@ -1739,22 +1934,6 @@ This may take up to a minute. 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 @@ -1779,24 +1958,20 @@ This may take up to a minute. 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 🎉. @@ -1851,6 +2026,26 @@ This may take up to a minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -1914,7 +2109,7 @@ This may take up to a minute. 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). @@ -1940,22 +2135,10 @@ This may take up to a minute. 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 新しい運転画面 @@ -1985,16 +2168,8 @@ This may take up to a minute. 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. @@ -2005,8 +2180,34 @@ This may take up to a minute. 運転者の常時モニタリング - 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 @@ -2017,55 +2218,86 @@ This may take up to a minute. - 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. + + + + 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: + + + + 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. - Updater + TreeOptionDialog - Update Required - アップデートが必要です + Select + 選択 - 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です。 + Cancel + キャンセル + + + + VisualsPanel + + Show Blind Spot Warnings + - Connect to Wi-Fi - Wi-Fiに接続 + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. + - Install - インストール + Changing this setting will restart openpilot if the car is powered on. + この設定を変更すると車の電源が入っている場合はopenpilotが再起動します。 - Back - 戻る + Off + - Loading... - 読み込み中... + Distance + - Reboot - 再起動 + Speed + - Update failed - 更新失敗 + Time + + + + All + + + + Display Metrics Below Chevron + + + + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2075,12 +2307,12 @@ This may take up to a minute. 開く - <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 89f30ea545..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 사용 시 대용량 데이터 업로드 방지 @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,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 뒤로 @@ -160,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 @@ -169,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 @@ -179,6 +202,22 @@ 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를 참조하세요. + + 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. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - 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. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW 다시보기 - - Review the rules, features, and limitations of sunnypilot - sunnypilot의 규칙, 기능 및 제한 다시 확인 - Are you sure you want to review the training guide? 트레이닝 가이드를 다시 확인하시겠습니까? @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off 전원 끄기 - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilot 장치는 좌우 4°, 위로 5°, 아래로 9° 이내 각도로 장착되어야 합니다. sunnypilot은 지속적으로 자동 보정되며 재설정은 거의 필요하지 않습니다. - Your device is pointed %1° %2 and %3° %4. 사용자의 장치는 %2 %1° 및 %4 %3° 의 방향으로 장착되어 있습니다. @@ -356,15 +387,51 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 - Resetting calibration will restart openpilot if the car is powered on. + 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 @@ -381,6 +448,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? 트레이닝 가이드를 다시 확인하시겠습니까? @@ -393,6 +464,19 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 재부팅 @@ -411,7 +495,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - 확인 + Are you sure you want to enter Always Offroad mode? @@ -421,22 +505,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. @@ -449,6 +517,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + ⁍ Default: Device will boot/wake-up normally & will be ready to engage. + + + + ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +572,21 @@ Please use caution when using this feature. Only use the blinker when traffic an 카메라 시작 중 + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,28 +600,28 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - 🔥 파이어호스 모드 🔥 - 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. - 최대한의 효과를 얻으려면 매주 장치를 실내로 가져와 좋은 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. @@ -526,7 +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 + 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. @@ -634,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). @@ -651,6 +762,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -658,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. @@ -673,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. @@ -701,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. @@ -713,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. @@ -760,31 +894,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 선택 - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -800,21 +947,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -823,6 +966,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. + + Model download has started in the background. @@ -843,6 +990,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -962,14 +1117,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 @@ -984,32 +1131,50 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 - - - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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. + +%1 @@ -1029,11 +1194,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - 오픈파일럿을 사용할수없습니다 + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY 핸들을 잡아주세요 @@ -1050,6 +1218,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive 시스템이 응답하지않습니다 + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + 확인 + + + Country + + + + SELECT + 선택 + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + 업데이트 + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1136,7 +1439,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - 확인 + Cancel @@ -1183,7 +1486,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Become a comma prime member at connect.comma.ai - connect.comma.ai에 접속하여 comma prime 회원으로 등록하세요 + connect.comma.ai에서 comma prime 사용자로 등록하세요 PRIME FEATURES: @@ -1199,7 +1502,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 1 year of drive storage - 1년간 드라이브 로그 저장 + 1년간 주행 로그 저장 Remote snapshots @@ -1219,10 +1522,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1245,46 +1544,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 + @@ -1345,29 +1607,13 @@ This may take up to a minute. 소프트웨어 - Trips + Models - - Vehicle - - - - Developer - 개발자 - - - Firehose - 파이어호스 - Steering - - Models - - Cruise @@ -1376,100 +1622,25 @@ This may take up to a minute. Visuals - - - Setup - WARNING: Low Voltage - 경고: 전압이 낮습니다 + OSM + - Power your device in a car with a harness or proceed at your own risk. - 장치를 하네스를 통해 차량 전원에 연결하세요. USB 전원에서는 예상치 못한 문제가 생길 수 있습니다. + Trips + - Power off - 전원 끄기 + Vehicle + - Continue - 계속 + Firehose + 파이어호스 - 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 - 커스텀 소프트웨어 + Developer + 개발자 @@ -1674,6 +1845,14 @@ This may take up to a minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1682,6 +1861,22 @@ This may take up to a minute. Select a branch 브랜치 선택 + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + 재부팅 + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1739,22 +1934,6 @@ This may take up to a minute. 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 @@ -1779,24 +1958,20 @@ This may take up to a minute. 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 🎉. @@ -1851,6 +2026,26 @@ This may take up to a minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -1903,11 +2098,11 @@ This may take up to a minute. 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> 에서 최신 약관을 읽은 후 계속하세요. + @@ -1940,29 +2135,21 @@ This may take up to a minute. 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 @@ -1980,22 +2167,10 @@ This may take up to a minute. 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. 운전 시각화는 일부 회전을 더 잘 보여주기 위해 저속에서 도로를 향한 광각 카메라로 전환됩니다. 우측 상단에 실험 모드 로고가 표시됩니다. @@ -2005,8 +2180,34 @@ This may take up to a minute. 상시 운전자 모니터링 - 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 @@ -2017,55 +2218,86 @@ This may take up to a minute. - 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. + + + + 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: + + + + 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. - Updater + TreeOptionDialog - Update Required - 업데이트 필요 + Select + 선택 - 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입니다. + Cancel + 취소 + + + + VisualsPanel + + Show Blind Spot Warnings + - Connect to Wi-Fi - Wi-Fi 연결 + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. + - Install - 설치 + Changing this setting will restart openpilot if the car is powered on. + 이 설정을 변경하면 차량이 재가동된후 오픈파일럿이 시작됩니다. - Back - 뒤로 + Off + - Loading... - 로딩 중... + Distance + - Reboot - 재부팅 + Speed + - Update failed - 업데이트 실패 + Time + + + + All + + + + Display Metrics Below Chevron + + + + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2075,12 +2307,12 @@ This may take up to a minute. 열기 - <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 bc9e50ee0a..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 @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,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 @@ -160,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 @@ -171,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 @@ -179,6 +202,22 @@ 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. + + 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. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - 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. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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? @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off Desligar - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - O sunnypilot requer que o dispositivo seja montado dentro de 4° esquerda ou direita e dentro de 5° para cima ou 9° para baixo. O sunnypilot está continuamente calibrando, resetar raramente é necessário. - Your device is pointed %1° %2 and %3° %4. Seu dispositivo está montado %1° %2 e %3° %4. @@ -356,15 +387,51 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 - Resetting calibration will restart openpilot if the car is powered on. + 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 @@ -381,6 +448,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? Tem certeza que quer rever o treinamento? @@ -393,6 +464,19 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 @@ -411,7 +495,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - Confirmar + Are you sure you want to enter Always Offroad mode? @@ -421,22 +505,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. @@ -449,6 +517,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + ⁍ Default: Device will boot/wake-up normally & will be ready to engage. + + + + ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +572,21 @@ Please use caution when using this feature. Only use the blinker when traffic an câmera iniciando + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,10 +600,6 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - 🔥 Modo Firehose 🔥 - Firehose Mode: ACTIVE Modo Firehose: ATIVO @@ -509,10 +608,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. @@ -520,6 +615,14 @@ Please use caution when using this feature. Only use the blinker when traffic an <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. @@ -527,8 +630,8 @@ 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 + 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. + @@ -636,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). @@ -653,6 +764,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -660,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. @@ -675,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. @@ -703,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. @@ -715,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. @@ -762,31 +896,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECIONE - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -802,21 +949,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -825,6 +968,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. + + Model download has started in the background. @@ -845,6 +992,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -964,14 +1119,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 @@ -986,32 +1133,50 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. - - - 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 - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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. + + + + 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. + + + + 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. + +%1 @@ -1031,11 +1196,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - sunnypilot Indisponível + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY ASSUMA IMEDIATAMENTE @@ -1052,6 +1220,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive Sistema sem Resposta + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + VERIFICAR + + + Country + + + + SELECT + SELECIONE + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + ATUALIZAÇÃO + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1138,7 +1441,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - Confirmar + Cancel @@ -1221,10 +1524,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1250,46 +1549,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 + @@ -1350,29 +1612,13 @@ Isso pode levar até um minuto. Software - Trips + Models - - Vehicle - - - - Developer - Desenvdor - - - Firehose - Firehose - Steering - - Models - - Cruise @@ -1381,100 +1627,25 @@ Isso pode levar até um minuto. Visuals - - - Setup - WARNING: Low Voltage - ALERTA: Baixa Voltagem + OSM + - 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. + Trips + - Power off - Desligar + Vehicle + - Continue - Continuar + Firehose + Firehose - 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 + Developer + Desenvdor @@ -1679,6 +1850,14 @@ Isso pode levar até um minuto. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1687,6 +1866,22 @@ Isso pode levar até um minuto. Select a branch Selecione uma branch + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + Reiniciar + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1744,22 +1939,6 @@ Isso pode levar até um minuto. 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 @@ -1784,24 +1963,20 @@ Isso pode levar até um minuto. 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 🎉. @@ -1856,6 +2031,26 @@ Isso pode levar até um minuto. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -1908,11 +2103,11 @@ Isso pode levar até um minuto. 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. + @@ -1945,22 +2140,10 @@ Isso pode levar até um minuto. 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 @@ -1969,6 +2152,10 @@ Isso pode levar até um minuto. 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 @@ -1985,22 +2172,10 @@ Isso pode levar até um minuto. 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. @@ -2010,8 +2185,34 @@ Isso pode levar até um minuto. 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 @@ -2022,55 +2223,86 @@ Isso pode levar até um minuto. - 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. + + + + 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: + + + + 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. - Updater + TreeOptionDialog - Update Required - Atualização Necessária + Select + Selecione - 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. + Cancel + Cancelar + + + + VisualsPanel + + Show Blind Spot Warnings + - Connect to Wi-Fi - Conecte-se ao Wi-Fi + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. + - Install - Instalar + 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. - Back - Voltar + Off + - Loading... - Carregando... + Distance + - Reboot - Reiniciar + Speed + - Update failed - Falha na atualização + Time + + + + All + + + + Display Metrics Below Chevron + + + + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2080,12 +2312,12 @@ Isso pode levar até um minuto. 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 2c3d0bdb6c..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 รีบูตและอัปเดต @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,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 ย้อนกลับ @@ -160,23 +171,51 @@ 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 - + โหมดการควบคุมการเร่ง/เบรค + + + 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. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - 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. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW ทบทวน - - Review the rules, features, and limitations of sunnypilot - ตรวจสอบกฎ คุณสมบัติ และข้อจำกัดของ sunnypilot - Are you sure you want to review the training guide? คุณแน่ใจหรือไม่ว่าต้องการทบทวนคู่มือการใช้งาน? @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off ปิดเครื่อง - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilot กำหนดให้ติดตั้งอุปกรณ์ โดยสามารถเอียงด้านซ้ายหรือขวาไม่เกิน 4° และเอียงขึ้นด้านบนไม่เกิน 5° หรือเอียงลงด้านล่างไม่เกิน 9° sunnypilot ทำการคาลิเบรทอย่างต่อเนื่อง แทบจะไม่จำเป็นต้องทำการรีเซ็ตการคาลิเบรท - Your device is pointed %1° %2 and %3° %4. อุปกรณ์ของคุณเอียงไปทาง %2 %1° และ %4 %3° @@ -359,12 +390,44 @@ Please use caution when using this feature. Only use the blinker when traffic an - Resetting calibration will restart openpilot if the car is powered on. + 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. + + + + + +Steering lag calibration is complete. + + + + Steering torque response calibration is %1% complete. + + + + 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. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -381,6 +444,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? คุณแน่ใจหรือไม่ว่าต้องการทบทวนคู่มือการใช้งาน? @@ -393,6 +460,19 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 รีบูต @@ -411,7 +491,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - ยืนยัน + Are you sure you want to enter Always Offroad mode? @@ -421,22 +501,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. @@ -449,6 +513,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + ⁍ Default: Device will boot/wake-up normally & will be ready to engage. + + + + ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +568,21 @@ Please use caution when using this feature. Only use the blinker when traffic an กำลังเปิดกล้อง + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,28 +596,28 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - - 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. @@ -526,7 +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 + 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. @@ -634,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). @@ -651,6 +758,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -658,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. @@ -673,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. @@ -701,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. @@ -713,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. @@ -760,31 +890,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp เลือก - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -800,21 +943,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -823,6 +962,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. + + Model download has started in the background. @@ -843,6 +986,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -966,14 +1117,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 @@ -988,28 +1131,46 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 ตรวจพบการเปลี่ยนแปลงของตำแหน่งที่ติดตั้ง กรุณาตรวจสอบว่าได้เลื่อนอุปกรณ์เข้ากับจุดติดตั้งจนสุดแล้ว และจุดติดตั้งได้ยึดติดกับกระจกหน้าอย่างแน่นหนา + - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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. + +%1 @@ -1029,11 +1190,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - sunnypilot ไม่สามารถใช้งานได้ + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY เข้าควบคุมรถเดี๋ยวนี้ @@ -1044,10 +1208,145 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Waiting to start - + รอเริ่มทำงาน System Unresponsive + ระบบไม่ตอบสนอง + + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + ตรวจสอบ + + + Country + + + + SELECT + เลือก + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + อัปเดต + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: @@ -1071,7 +1370,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Please connect to Wi-Fi to complete initial pairing - + กรุณาเชื่อมต่อ Wi-Fi เพื่อทำการจับคู่ครั้งแรกให้เสร็จสิ้น @@ -1136,7 +1435,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - ยืนยัน + Cancel @@ -1203,7 +1502,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Remote snapshots - + ภาพถ่ายระยะไกล @@ -1219,10 +1518,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1245,46 +1540,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 + @@ -1311,11 +1569,11 @@ This may take up to a minute. Developer - + นักพัฒนา Firehose - + สายยางดับเพลิง @@ -1345,29 +1603,13 @@ This may take up to a minute. ซอฟต์แวร์ - Trips - - - - Vehicle - - - - Developer - - - - Firehose + Models Steering - - Models - - Cruise @@ -1376,100 +1618,25 @@ This may take up to a minute. Visuals - - - Setup - WARNING: Low Voltage - คำเตือน: แรงดันแบตเตอรี่ต่ำ + OSM + - Power your device in a car with a harness or proceed at your own risk. - โปรดต่ออุปกรณ์ของคุณเข้ากับสายควบคุมในรถยนต์ หรือดำเนินการด้วยความเสี่ยงของคุณเอง + Trips + - Power off - ปิดเครื่อง + Vehicle + - Continue - ดำเนินการต่อ + Firehose + สายยางดับเพลิง - 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 - ซอฟต์แวร์ที่กำหนดเอง + Developer + นักพัฒนา @@ -1674,6 +1841,14 @@ This may take up to a minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1682,6 +1857,22 @@ This may take up to a minute. Select a branch เลือก Branch + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + รีบูต + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1739,22 +1930,6 @@ This may take up to a minute. 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 @@ -1779,24 +1954,20 @@ This may take up to a minute. 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 🎉. @@ -1851,6 +2022,26 @@ This may take up to a minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -1940,22 +2131,10 @@ This may take up to a minute. 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 การแสดงภาพการขับขี่แบบใหม่ @@ -1964,6 +2143,10 @@ This may take up to a minute. 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 ดุดัน @@ -1980,32 +2163,46 @@ This may take up to a minute. 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. @@ -2017,11 +2214,53 @@ This may take up to a minute. - Enable sunnypilot - เปิดใช้งาน sunnypilot + When enabled, pressing the accelerator pedal will disengage 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 driver monitoring even when sunnypilot is not engaged. + + + + 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 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: + + + + 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. + + + + + TreeOptionDialog + + Select + เลือก + + + Cancel + ยกเลิก + + + + VisualsPanel + + Show Blind Spot Warnings + + + + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. @@ -2029,58 +2268,47 @@ This may take up to a minute. - openpilot longitudinal control may come in a future update. + Off - - - Updater - Update Required - จำเป็นต้องอัปเดต + Distance + - 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 + Speed + - Connect to Wi-Fi - เชื่อมต่อกับ Wi-Fi + Time + - Install - ติดตั้ง + All + - Back - ย้อนกลับ + Display Metrics Below Chevron + - Loading... - กำลังโหลด... - - - Reboot - รีบูต - - - Update failed - การอัปเดตล้มเหลว + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + 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 fd06e81ef1..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 @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,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 @@ -160,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 @@ -171,6 +186,14 @@ 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). + + Enable ADB @@ -179,6 +202,22 @@ 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. + + 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. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - 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. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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? @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off Sistemi kapat - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilot, cihazın 4° sola veya 5° yukarı yada 9° aşağı bakıcak şekilde monte edilmesi gerekmektedir. sunnypilot sürekli kendisini kalibre edilmektedir ve nadiren sıfırlama gerebilir. - Your device is pointed %1° %2 and %3° %4. Cihazınız %1° %2 ve %3° %4 yönünde ayarlı @@ -359,12 +390,44 @@ Please use caution when using this feature. Only use the blinker when traffic an - Resetting calibration will restart openpilot if the car is powered on. + 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. + + + + + +Steering lag calibration is complete. + + + + Steering torque response calibration is %1% complete. + + + + 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. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -381,6 +444,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? Eğitim kılavuzunu incelemek istediğinizden emin misiniz? @@ -393,6 +460,19 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 @@ -411,7 +491,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - Onayla + Are you sure you want to enter Always Offroad mode? @@ -421,22 +501,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. @@ -449,6 +513,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + ⁍ Default: Device will boot/wake-up normally & will be ready to engage. + + + + ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +568,21 @@ Please use caution when using this feature. Only use the blinker when traffic an kamera başlatılıyor + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,10 +596,6 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - - Firehose Mode: ACTIVE @@ -509,16 +604,20 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. @@ -526,7 +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 + 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. @@ -634,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). @@ -651,6 +758,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -658,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. @@ -673,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. @@ -701,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. @@ -713,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. @@ -760,31 +890,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -800,21 +943,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -823,6 +962,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. + + Model download has started in the background. @@ -843,6 +986,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -966,14 +1117,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 @@ -987,16 +1130,32 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. @@ -1008,7 +1167,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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. + +%1 @@ -1028,11 +1189,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable + ALWAYS OFFROAD ACTIVE + + + OnroadAlerts TAKE CONTROL IMMEDIATELY @@ -1049,6 +1213,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + KONTROL ET + + + Country + + + + SELECT + + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + GÜNCELLE + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1135,7 +1434,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - Onayla + Cancel @@ -1218,10 +1517,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1244,44 +1539,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 @@ -1343,29 +1602,13 @@ This may take up to a minute. Yazılım - Trips - - - - Vehicle - - - - Developer - - - - Firehose + Models Steering - - Models - - Cruise @@ -1374,99 +1617,24 @@ This may take up to a minute. Visuals - - - 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. + OSM - No custom software found at this URL. + Trips - Select a language - Dil seçin - - - Choose Software to Install + Vehicle - sunnypilot - sunnypilot + Firehose + - Custom Software + Developer @@ -1672,6 +1840,14 @@ This may take up to a minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1680,6 +1856,22 @@ This may take up to a minute. Select a branch + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + Yeniden başlat + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1737,22 +1929,6 @@ This may take up to a minute. 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 @@ -1777,24 +1953,20 @@ This may take up to a minute. 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 🎉. @@ -1849,6 +2021,26 @@ This may take up to a minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -1934,10 +2126,6 @@ This may take up to a minute. 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 @@ -1962,18 +2150,10 @@ This may take up to a minute. 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 @@ -1983,15 +2163,7 @@ This may take up to a minute. - 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. @@ -2003,7 +2175,33 @@ This may take up to a minute. - 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. @@ -2015,11 +2213,53 @@ This may take up to a minute. - Enable sunnypilot - sunnypilot'u aktifleştir + When enabled, pressing the accelerator pedal will disengage 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 driver monitoring even when sunnypilot is not engaged. + + + + 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 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: + + + + 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. + + + + + TreeOptionDialog + + Select + Seç + + + Cancel + + + + + VisualsPanel + + Show Blind Spot Warnings + + + + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. @@ -2027,43 +2267,32 @@ This may take up to a minute. - openpilot longitudinal control may come in a future update. + Off - - - Updater - Update Required - Güncelleme yapılması gerekli + Distance + - 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. + Speed + - Connect to Wi-Fi - Wi-Fi ağına bağlan + Time + - Install - Yükle + All + - Back - Geri + Display Metrics Below Chevron + - Loading... - Yükleniyor... - - - Reboot - Yeniden başlat - - - Update failed - Güncelleme başarız oldu + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2073,11 +2302,11 @@ This may take up to a minute. - <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 06c6c67152..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 网络上,防止上传大数据 @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,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 返回 @@ -160,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 @@ -171,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 @@ -179,6 +202,22 @@ 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)。 + + 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. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - 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. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW 查看 - - Review the rules, features, and limitations of sunnypilot - 查看 sunnypilot 的使用规则,以及其功能和限制 - Are you sure you want to review the training guide? 您确定要查看新手指南吗? @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off 关机 - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilot要求设备安装的偏航角在左4°和右4°之间,俯仰角在上5°和下9°之间。一般来说,sunnypilot会持续更新校准,很少需要重置。 - Your device is pointed %1° %2 and %3° %4. 您的设备校准为%1° %2、%3° %4。 @@ -324,7 +355,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Reboot - 取消sunnypilot以重新启动 + 取消openpilot以重新启动 Are you sure you want to power off? @@ -332,7 +363,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Power Off - 取消sunnypilot以关机 + 取消openpilot以关机 Reset @@ -356,15 +387,51 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 - Resetting calibration will restart openpilot if the car is powered on. + 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 @@ -381,6 +448,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? 您确定要查看新手指南吗? @@ -393,6 +464,19 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 重启 @@ -411,7 +495,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - 确认 + Are you sure you want to enter Always Offroad mode? @@ -421,22 +505,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. @@ -449,6 +517,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + ⁍ Default: Device will boot/wake-up normally & will be ready to engage. + + + + ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +572,21 @@ Please use caution when using this feature. Only use the blinker when traffic an 正在启动相机 + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,28 +600,28 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - 🔥 训练数据上传模式 🔥 - 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. @@ -526,8 +629,8 @@ 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>:请连接到不限流量的网络 + 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. + @@ -634,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). @@ -651,6 +762,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -658,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. @@ -673,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. @@ -701,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. @@ -713,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. @@ -760,31 +894,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 选择 - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -800,21 +947,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -823,6 +966,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. + + Model download has started in the background. @@ -843,6 +990,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -962,14 +1117,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 @@ -984,32 +1131,50 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 固态硬盘可能会消耗更多电力并更易过热。 - - - 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 - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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 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. + + + + 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. + +%1 @@ -1029,11 +1194,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - 无法使用 sunnypilot + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY 立即接管 @@ -1050,6 +1218,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive 系统无响应 + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + 查看 + + + Country + + + + SELECT + 选择 + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + 更新 + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1136,7 +1439,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - 确认 + Cancel @@ -1219,10 +1522,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1245,46 +1544,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 + @@ -1315,7 +1577,7 @@ This may take up to a minute. Firehose - 训练上传 + Firehose @@ -1345,29 +1607,13 @@ This may take up to a minute. 软件 - Trips + Models - - Vehicle - - - - Developer - 开发人员 - - - Firehose - 训练上传 - Steering - - Models - - Cruise @@ -1376,100 +1622,25 @@ This may take up to a minute. Visuals - - - Setup - WARNING: Low Voltage - 警告:低电压 + OSM + - Power your device in a car with a harness or proceed at your own risk. - 请使用car harness线束为您的设备供电,或自行承担风险。 + Trips + - Power off - 关机 + Vehicle + - Continue - 继续 + Firehose + Firehose - 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 - 定制软件 + Developer + 开发人员 @@ -1674,6 +1845,14 @@ This may take up to a minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1682,6 +1861,22 @@ This may take up to a minute. Select a branch 选择分支 + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + 重启 + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1739,22 +1934,6 @@ This may take up to a minute. 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 @@ -1779,24 +1958,20 @@ This may take up to a minute. 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 🎉. @@ -1851,6 +2026,26 @@ This may take up to a minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -1903,11 +2098,11 @@ This may take up to a minute. 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>。 + @@ -1940,22 +2135,10 @@ This may take up to a minute. 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 新驾驶视角 @@ -1964,6 +2147,10 @@ This may take up to a minute. 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 积极 @@ -1980,22 +2167,10 @@ This may take up to a minute. 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. 在低速时,驾驶可视化将转换为道路朝向的广角摄像头,以更好地展示某些转弯。测试模式标志也将显示在右上角。 @@ -2005,8 +2180,34 @@ This may take up to a minute. 驾驶员监控常开 - 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 @@ -2017,55 +2218,86 @@ This may take up to a minute. - 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. + + + + 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: + + + + 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. - Updater + TreeOptionDialog - Update Required - 需要更新 + Select + 选择 - 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。 + Cancel + 取消 + + + + VisualsPanel + + Show Blind Spot Warnings + - Connect to Wi-Fi - 连接到WiFi + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. + - Install - 安装 + Changing this setting will restart openpilot if the car is powered on. + 如果车辆已通电,更改此设置将会重新启动 openpilot。 - Back - 返回 + Off + - Loading... - 正在加载…… + Distance + - Reboot - 重启 + Speed + - Update failed - 更新失败 + Time + + + + All + + + + Display Metrics Below Chevron + + + + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2075,12 +2307,12 @@ This may take up to a minute. 开启 - <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 c46157e78e..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 網路時,防止上傳大量數據 @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,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 回上頁 @@ -160,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 @@ -171,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 @@ -179,6 +202,22 @@ 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)。 + + 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. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - 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. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW 觀看 - - Review the rules, features, and limitations of sunnypilot - 觀看 sunnypilot 的使用規則、功能和限制 - Are you sure you want to review the training guide? 您確定要觀看使用教學嗎? @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off 關機 - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilot 需要將裝置固定在左右偏差 4° 以內,朝上偏差 5° 以內或朝下偏差 9° 以內。鏡頭在後台會持續自動校準,很少有需要重置的情況。 - Your device is pointed %1° %2 and %3° %4. 你的裝置目前朝%2 %1° 以及朝%4 %3° 。 @@ -356,15 +387,51 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 - Resetting calibration will restart openpilot if the car is powered on. + 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 @@ -381,6 +448,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? 您確定要觀看使用教學嗎? @@ -393,6 +464,19 @@ Please use caution when using this feature. Only use the blinker when traffic an 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 重新啟動 @@ -411,7 +495,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - 確認 + Are you sure you want to enter Always Offroad mode? @@ -421,22 +505,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. @@ -449,6 +517,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + ⁍ Default: Device will boot/wake-up normally & will be ready to engage. + + + + ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +572,21 @@ Please use caution when using this feature. Only use the blinker when traffic an 開啟相機中 + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,28 +600,28 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - 🔥 訓練資料上傳模式 🔥 - 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. @@ -526,8 +629,8 @@ 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>:請連接到不按流量計費的網絡 + 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. + @@ -634,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). @@ -651,6 +762,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -658,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. @@ -673,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. @@ -701,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. @@ -713,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. @@ -760,31 +894,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 選取 - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -800,21 +947,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -823,6 +966,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. + + Model download has started in the background. @@ -843,6 +990,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -962,14 +1117,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 @@ -984,32 +1131,50 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 固態硬碟可能會消耗更多電力並更易過熱。 - - - 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 - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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 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. + + + + 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. + +%1 @@ -1029,11 +1194,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - 無法使用 sunnypilot + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY 立即接管 @@ -1050,6 +1218,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive 系統無回應 + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + 檢查 + + + Country + + + + SELECT + 選取 + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + 更新 + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1136,7 +1439,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - 確認 + Cancel @@ -1219,10 +1522,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1245,46 +1544,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 + @@ -1315,7 +1577,7 @@ This may take up to a minute. Firehose - 訓練上傳 + Firehose @@ -1345,29 +1607,13 @@ This may take up to a minute. 軟體 - Trips + Models - - Vehicle - - - - Developer - 開發人員 - - - Firehose - 訓練上傳 - Steering - - Models - - Cruise @@ -1376,100 +1622,25 @@ This may take up to a minute. Visuals - - - Setup - WARNING: Low Voltage - 警告:電壓過低 + OSM + - Power your device in a car with a harness or proceed at your own risk. - 請使用車上 harness 提供的電源,若繼續的話您需要自擔風險。 + Trips + - Power off - 關機 + Vehicle + - Continue - 繼續 + Firehose + Firehose - 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 - 自訂軟體 + Developer + 開發人員 @@ -1674,6 +1845,14 @@ This may take up to a minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1682,6 +1861,22 @@ This may take up to a minute. Select a branch 選取一個分支 + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + 重新啟動 + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1739,22 +1934,6 @@ This may take up to a minute. 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 @@ -1779,24 +1958,20 @@ This may take up to a minute. 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 🎉. @@ -1851,6 +2026,26 @@ This may take up to a minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + SunnylinkSponsorPopup @@ -1903,11 +2098,11 @@ This may take up to a minute. 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>。 + @@ -1940,22 +2135,10 @@ This may take up to a minute. 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 新的駕駛視覺介面 @@ -1964,6 +2147,10 @@ This may take up to a minute. 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 積極 @@ -1980,22 +2167,10 @@ This may take up to a minute. 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. 在低速時,駕駛可視化將切換至道路朝向的廣角攝影機,以更好地顯示某些彎道。在右上角還會顯示「實驗模式」的標誌。 @@ -2005,8 +2180,34 @@ This may take up to a minute. 駕駛監控常開 - 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 @@ -2017,55 +2218,86 @@ This may take up to a minute. - 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. + + + + 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: + + + + 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. - Updater + TreeOptionDialog - Update Required - 系統更新 + Select + 選擇 - 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。 + Cancel + 取消 + + + + VisualsPanel + + Show Blind Spot Warnings + - Connect to Wi-Fi - 連接到無線網路 + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. + - Install - 安裝 + Changing this setting will restart openpilot if the car is powered on. + 若車輛電源為開啟狀態,變更此設定將會重新啟動 openpilot。 - Back - 回上頁 + Off + - Loading... - 載入中… + Distance + - Reboot - 重新啟動 + Speed + - Update failed - 更新失敗 + Time + + + + All + + + + Display Metrics Below Chevron + + + + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2075,12 +2307,12 @@ This may take up to a minute. 開啟 - <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/ui.cc b/selfdrive/ui/ui.cc index c9dc70cdff..4b8f79785b 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -61,6 +61,9 @@ void update_state(UIState *s) { scene.light_sensor = -1; } scene.started = sm["deviceState"].getDeviceState().getStarted() && scene.ignition; + + auto params = Params(); + scene.recording_audio = params.getBool("RecordAudio") && scene.started; } void ui_update_params(UIState *s) { @@ -164,8 +167,9 @@ void Device::setAwake(bool on) { } void Device::resetInteractiveTimeout(int timeout) { + int customTimeout = QString::fromStdString(Params().get("InteractivityTimeout")).toInt(); if (timeout == -1) { - timeout = (ignition_on ? 10 : 30); + timeout = customTimeout == 0 ? (ignition_on ? 10 : 30) : customTimeout; } interactive_timeout = timeout * UI_FREQ; } diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index 4ead8aecb9..e78b573b66 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -62,7 +62,7 @@ typedef struct UIScene { cereal::LongitudinalPersonality personality; float light_sensor = -1; - bool started, ignition, is_metric; + bool started, ignition, is_metric, recording_audio; uint64_t started_frame; } UIScene; diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index 3738ea6d5d..c2bb5f77a4 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -1,12 +1,20 @@ import pyray as rl +import numpy as np import time +import threading from collections.abc import Callable from enum import Enum from cereal import messaging, log +from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params, UnknownKeyName +from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.lib.prime_state import PrimeState +from openpilot.system.ui.lib.application import DEFAULT_FPS +from openpilot.system.hardware import HARDWARE +from openpilot.system.ui.lib.application import gui_app UI_BORDER_SIZE = 30 +BACKLIGHT_OFFROAD = 50 class UIStatus(Enum): @@ -139,10 +147,15 @@ class UIState: class Device: def __init__(self): self._ignition = False - self._interaction_time: float = 0.0 + self._interaction_time: float = -1 self._interactive_timeout_callbacks: list[Callable] = [] self._prev_timed_out = False - self.reset_interactive_timeout() + self._awake = False + + self._offroad_brightness: int = BACKLIGHT_OFFROAD + self._last_brightness: int = 0 + self._brightness_filter = FirstOrderFilter(BACKLIGHT_OFFROAD, 10.00, 1 / DEFAULT_FPS) + self._brightness_thread: threading.Thread | None = None def reset_interactive_timeout(self, timeout: int = -1) -> None: if timeout == -1: @@ -153,18 +166,64 @@ class Device: self._interactive_timeout_callbacks.append(callback) def update(self): + # do initial reset + if self._interaction_time <= 0: + self.reset_interactive_timeout() + + self._update_brightness() + self._update_wakefulness() + + def set_offroad_brightness(self, brightness: int): + # TODO: not yet used, should be used in prime widget for QR code, etc. + self._offroad_brightness = min(max(brightness, 0), 100) + + def _update_brightness(self): + clipped_brightness = self._offroad_brightness + + if ui_state.started and ui_state.light_sensor >= 0: + clipped_brightness = ui_state.light_sensor + + # CIE 1931 - https://www.photonstophotos.net/GeneralTopics/Exposure/Psychometric_Lightness_and_Gamma.htm + if clipped_brightness <= 8: + clipped_brightness = clipped_brightness / 903.3 + else: + clipped_brightness = ((clipped_brightness + 16.0) / 116.0) ** 3.0 + + clipped_brightness = float(np.clip(100 * clipped_brightness, 10, 100)) + + brightness = round(self._brightness_filter.update(clipped_brightness)) + if not self._awake: + brightness = 0 + + if brightness != self._last_brightness: + if self._brightness_thread is None or not self._brightness_thread.is_alive(): + cloudlog.debug(f"setting display brightness {brightness}") + self._brightness_thread = threading.Thread(target=HARDWARE.set_screen_brightness, args=(brightness,)) + self._brightness_thread.start() + self._last_brightness = brightness + + def _update_wakefulness(self): # Handle interactive timeout ignition_just_turned_off = not ui_state.ignition and self._ignition self._ignition = ui_state.ignition - interaction_timeout = time.monotonic() > self._interaction_time - if ignition_just_turned_off or rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT): + if ignition_just_turned_off or any(ev.left_down for ev in gui_app.mouse_events): self.reset_interactive_timeout() - elif interaction_timeout and not self._prev_timed_out: + + interaction_timeout = time.monotonic() > self._interaction_time + if interaction_timeout and not self._prev_timed_out: for callback in self._interactive_timeout_callbacks: callback() self._prev_timed_out = interaction_timeout + self._set_awake(ui_state.ignition or not interaction_timeout) + + def _set_awake(self, on: bool): + if on != self._awake: + self._awake = on + cloudlog.debug(f"setting display power {int(on)}") + HARDWARE.set_display_power(on) + # Global instance ui_state = UIState() 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..9618768957 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,20 +31,13 @@ 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.draw_rectangle_rounded(rect, 0.08, 20, rl.WHITE) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) self._draw_gradient_background(rect) @@ -61,7 +53,7 @@ class ExperimentalModeButton(Widget): text_x = rect.x + self.horizontal_padding text_y = rect.y + rect.height / 2 - 45 // 2 # Center vertically - rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), text, rl.Vector2(int(text_x), int(text_y)), 45, 0, rl.Color(0, 0, 0, 255)) + rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), text, rl.Vector2(int(text_x), int(text_y)), 45, 0, rl.BLACK) # Draw icon (right aligned) icon_x = rect.x + rect.width - self.horizontal_padding - self.img_width @@ -71,4 +63,4 @@ class ExperimentalModeButton(Widget): # Draw current mode icon current_icon = self.experimental_pixmap if self.experimental_mode else self.chill_pixmap source_rect = rl.Rectangle(0, 0, current_icon.width, current_icon.height) - rl.draw_texture_pro(current_icon, source_rect, icon_rect, rl.Vector2(0, 0), 0, rl.Color(255, 255, 255, 255)) + rl.draw_texture_pro(current_icon, source_rect, icon_rect, rl.Vector2(0, 0), 0, rl.WHITE) 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..6b601f6dff 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): @@ -36,7 +36,7 @@ class PrimeWidget(Widget): font = gui_app.font(FontWeight.LIGHT) wrapped_text = "\n".join(wrap_text(font, "Become a comma prime member at connect.comma.ai", 56, int(w))) text_size = measure_text_cached(font, wrapped_text, 56) - rl.draw_text_ex(font, wrapped_text, rl.Vector2(x, desc_y), 56, 0, rl.Color(255, 255, 255, 255)) + rl.draw_text_ex(font, wrapped_text, rl.Vector2(x, desc_y), 56, 0, rl.WHITE) # Features section features_y = desc_y + text_size.y + 50 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/__init__.py b/sunnypilot/livedelay/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..8495dcee0a --- /dev/null +++ b/sunnypilot/livedelay/lagd_toggle.py @@ -0,0 +1,38 @@ +""" +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 + + +class LagdToggle: + def __init__(self, CP: structs.CarParams): + self.CP = CP + self.params = Params() + self.lag = 0.0 + + self.lagd_toggle = self.params.get_bool("LagdToggle") + self.software_delay = self.params.get("LagdToggleDelay", return_default=True) + + def read_params(self) -> None: + self.lagd_toggle = self.params.get_bool("LagdToggle") + self.software_delay = self.params.get("LagdToggleDelay", return_default=True) + + def update(self, lag_msg: log.LiveDelayData) -> None: + self.read_params() + + 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 + + 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/__init__.py b/sunnypilot/mapd/__init__.py index e69de29bb2..7ad6f74149 100644 --- a/sunnypilot/mapd/__init__.py +++ b/sunnypilot/mapd/__init__.py @@ -0,0 +1,5 @@ +import os +from openpilot.common.basedir import BASEDIR + +MAPD_BIN_DIR = os.path.join(BASEDIR, 'third_party/mapd_pfeiferj') +MAPD_PATH = os.path.join(MAPD_BIN_DIR, 'mapd') 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 a151dd4807..4cfb5ebbbf 100755 --- a/sunnypilot/mapd/mapd_installer.py +++ b/sunnypilot/mapd/mapd_installer.py @@ -16,32 +16,39 @@ from urllib.request import urlopen from cereal import messaging from openpilot.common.params import Params -from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH, MAPD_BIN_DIR from openpilot.system.hardware.hw import Paths from openpilot.common.spinner import Spinner from openpilot.system.version import is_prebuilt +from openpilot.sunnypilot.mapd import MAPD_PATH, MAPD_BIN_DIR import openpilot.system.sentry as sentry -VERSION = 'v1.9.0' +VERSION = 'v1.10.0' URL = f"https://github.com/pfeiferj/openpilot-mapd/releases/download/{VERSION}/mapd" +def update_installed_version(version: str, params: Params = None) -> None: + if params is None: + params = Params() + + params.put("MapdVersion", version) + + class MapdInstallManager: def __init__(self, spinner_ref: Spinner): self._spinner = spinner_ref + self._params = Params() def download(self) -> None: self.ensure_directories_exist() self._download_file() - self.update_installed_version(VERSION) + update_installed_version(VERSION, self._params) def check_and_download(self) -> None: if self.download_needed(): self.download() - @staticmethod - def download_needed() -> bool: - return not os.path.exists(MAPD_PATH) or MapdInstallManager.get_installed_version() != VERSION + def download_needed(self) -> bool: + return not os.path.exists(MAPD_PATH) or self.get_installed_version() != VERSION @staticmethod def ensure_directories_exist() -> None: @@ -82,13 +89,8 @@ class MapdInstallManager: temp_file.unlink() logging.error("Failed to download file after all retries") - @staticmethod - def update_installed_version(version: str) -> None: - Params().put("MapdVersion", version) - - @staticmethod - def get_installed_version() -> str: - return Params().get("MapdVersion", encoding="utf-8") or "" + def get_installed_version(self) -> str: + return str(self._params.get("MapdVersion")) def wait_for_internet_connection(self, return_on_failure: bool = False) -> bool: max_retries = 10 @@ -122,7 +124,7 @@ class MapdInstallManager: return if self.wait_for_internet_connection(return_on_failure=True): - self._spinner.update(f"Downloading pfeiferj's mapd [{install_manager.get_installed_version()}] => [{VERSION}].") + self._spinner.update(f"Downloading pfeiferj's mapd [{self.get_installed_version()}] => [{VERSION}].") time.sleep(0.1) self.check_and_download() self._spinner.close() @@ -146,7 +148,7 @@ if __name__ == "__main__": if is_prebuilt(): debug_msg = f"[DEBUG] This is prebuilt, no mapd install required. VERSION: [{VERSION}], Param [{install_manager.get_installed_version()}]" spinner.update(debug_msg) - install_manager.update_installed_version(VERSION) + update_installed_version(VERSION) else: spinner.update(f"Checking if mapd is installed and valid. Prebuilt [{is_prebuilt()}]") install_manager.non_prebuilt_install() diff --git a/sunnypilot/mapd/mapd_manager.py b/sunnypilot/mapd/mapd_manager.py index 342ba3fe7b..9ebf07a7f4 100755 --- a/sunnypilot/mapd/mapd_manager.py +++ b/sunnypilot/mapd/mapd_manager.py @@ -12,22 +12,20 @@ import os import glob import shutil -from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.realtime import Ratekeeper, config_realtime_process from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.sunnypilot.mapd.live_map_data.osm_map_data import OsmMapData from openpilot.system.hardware.hw import Paths +from openpilot.sunnypilot.mapd import MAPD_PATH +from openpilot.sunnypilot.mapd.mapd_installer import VERSION, update_installed_version # PFEIFER - MAPD {{ params = Params() mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else params # }} PFEIFER - MAPD -MAPD_BIN_DIR = os.path.join(BASEDIR, 'third_party/mapd_pfeiferj') -MAPD_PATH = os.path.join(MAPD_BIN_DIR, 'mapd') - def get_files_for_cleanup() -> list[str]: paths = [ @@ -58,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) @@ -100,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) @@ -117,6 +120,7 @@ def update_osm_db() -> None: def main_thread(): + update_installed_version(VERSION, params) config_realtime_process([0, 1, 2, 3], 5) rk = Ratekeeper(1, print_delay_threshold=None) diff --git a/sunnypilot/modeld/fill_model_msg.py b/sunnypilot/modeld/fill_model_msg.py index 608f24424d..dadffc8433 100644 --- a/sunnypilot/modeld/fill_model_msg.py +++ b/sunnypilot/modeld/fill_model_msg.py @@ -103,7 +103,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D fill_xyzt(orientation_rate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) # temporal pose - temporal_pose = modelV2.temporalPose + temporal_pose = modelV2.temporalPoseDEPRECATED temporal_pose.trans = net_output_data['plan'][0,0,Plan.VELOCITY].tolist() temporal_pose.transStd = net_output_data['plan_stds'][0,0,Plan.VELOCITY].tolist() temporal_pose.rot = net_output_data['plan'][0,0,Plan.ORIENTATION_RATE].tolist() diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py index d4bd96ff66..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.models.modeld_lagd import ModeldLagd 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,12 +58,13 @@ 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) bundle = get_active_bundle() overrides = {override.key: override.value for override in bundle.overrides} - self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".2")) + self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".0")) self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0")) model_paths = get_model_path() @@ -202,8 +204,6 @@ def main(demo=False): cloudlog.info("modeld got CarParams: %s", CP.brand) - modeld_lagd = ModeldLagd() - # 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/fill_model_msg.py b/sunnypilot/modeld_v2/fill_model_msg.py index 4d04d6e5ec..c7de698f61 100644 --- a/sunnypilot/modeld_v2/fill_model_msg.py +++ b/sunnypilot/modeld_v2/fill_model_msg.py @@ -10,8 +10,8 @@ SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') ConfidenceClass = log.ModelDataV2.ConfidenceClass -def get_curvature_from_output(output, vego, lat_action_t, current_generation=None): - if current_generation != 11: +def get_curvature_from_output(output, vego, lat_action_t, mlsim): + if not mlsim: if desired_curv := output.get('desired_curvature'): # If the model outputs the desired curvature, use that directly return float(desired_curv[0, 0]) @@ -100,7 +100,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) # temporal pose - temporal_pose = modelV2.temporalPose + temporal_pose = modelV2.temporalPoseDEPRECATED if 'sim_pose' in net_output_data: temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist() temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist() diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index 5b9c5ef9e5..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.models.modeld_lagd import ModeldLagd 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 @@ -54,10 +56,10 @@ class ModelState: raise model_bundle = get_active_bundle() - self.generation = model_bundle.generation + self.generation = model_bundle.generation if model_bundle is not None else None overrides = {override.key: override.value for override in model_bundle.overrides} - self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".2")) + self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".0")) self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0")) self.MIN_LAT_CONTROL_SPEED = 0.3 @@ -86,6 +88,10 @@ class ModelState: self.desire_reshape_dims = (self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], -1, self.numpy_inputs['desire'].shape[2]) + @property + def mlsim(self) -> bool: + return bool(self.generation is not None and self.generation >= 11) + def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: # Model decides when action is completed, so desire input is just a pulse triggered on rising edge @@ -151,7 +157,7 @@ class ModelState: self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:] self.full_prev_desired_curv[0,-1,:] = outputs['desired_curvature'][0, :] self.numpy_inputs[input_name_prev][:] = self.full_prev_desired_curv[0, self.temporal_idxs] - if self.generation == 11: + if self.mlsim: self.numpy_inputs[input_name_prev][:] = 0*self.full_prev_desired_curv[0, self.temporal_idxs] else: length = outputs['desired_curvature'][0].size @@ -165,7 +171,7 @@ class ModelState: action_t=long_action_t) desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS) - desired_curvature = get_curvature_from_output(model_output, v_ego, lat_action_t, self.generation) + desired_curvature = get_curvature_from_output(model_output, v_ego, lat_action_t, self.mlsim) if v_ego > self.MIN_LAT_CONTROL_SPEED: desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS) else: @@ -238,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 = ModeldLagd() - # TODO Move smooth seconds to action function long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS prev_action = log.ModelDataV2.Action() @@ -285,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))] @@ -322,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) @@ -334,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/modeld_v2/parse_model_outputs_split.py b/sunnypilot/modeld_v2/parse_model_outputs_split.py index 7fab66e038..ba8592ed18 100644 --- a/sunnypilot/modeld_v2/parse_model_outputs_split.py +++ b/sunnypilot/modeld_v2/parse_model_outputs_split.py @@ -1,5 +1,6 @@ import numpy as np from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants +from openpilot.sunnypilot.models.helpers import get_active_bundle def safe_exp(x, out=None): @@ -24,6 +25,8 @@ def softmax(x, axis=-1): class Parser: def __init__(self, ignore_missing=False): self.ignore_missing = ignore_missing + model_bundle = get_active_bundle() + self.generation = model_bundle.generation if model_bundle is not None else None def check_missing(self, outs, name): if name not in outs and not self.ignore_missing: @@ -88,37 +91,62 @@ class Parser: outs[name] = pred_mu_final.reshape(final_shape) outs[name + '_stds'] = pred_std_final.reshape(final_shape) + def _parse_plan_mhp(self, outs): + self.parse_mdn('plan', outs, in_N=SplitModelConstants.PLAN_MHP_N, out_N=SplitModelConstants.PLAN_MHP_SELECTION, + out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.PLAN_WIDTH)) + + def parse_dynamic_outputs(self, outs: dict[str, np.ndarray]) -> None: + if 'lead' in outs: + if self.generation >= 12 and \ + outs['lead'].shape[1] == 2 * SplitModelConstants.LEAD_MHP_SELECTION * SplitModelConstants.LEAD_TRAJ_LEN * SplitModelConstants.LEAD_WIDTH: + self.parse_mdn('lead', outs, in_N=0, out_N=0, + out_shape=(SplitModelConstants.LEAD_MHP_SELECTION, SplitModelConstants.LEAD_TRAJ_LEN, SplitModelConstants.LEAD_WIDTH)) + else: + self.parse_mdn('lead', outs, in_N=SplitModelConstants.LEAD_MHP_N, out_N=SplitModelConstants.LEAD_MHP_SELECTION, + out_shape=(SplitModelConstants.LEAD_TRAJ_LEN, SplitModelConstants.LEAD_WIDTH)) + if 'plan' in outs: + if self.generation >= 12 and \ + outs['plan'].shape[1] == 2 * SplitModelConstants.IDX_N * SplitModelConstants.PLAN_WIDTH: + self.parse_mdn('plan', outs, in_N=0, out_N=0, + out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH)) + else: + self._parse_plan_mhp(outs) + def split_outputs(self, outs: dict[str, np.ndarray]) -> None: + if 'desired_curvature' in outs: + self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.DESIRED_CURV_WIDTH,)) + if 'desire_pred' in outs: + self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(SplitModelConstants.DESIRE_PRED_LEN,SplitModelConstants.DESIRE_PRED_WIDTH)) + if 'desire_state' in outs: + self.parse_categorical_crossentropy('desire_state', outs, out_shape=(SplitModelConstants.DESIRE_PRED_WIDTH,)) if 'lane_lines' in outs: self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, - out_shape=(SplitModelConstants.NUM_LANE_LINES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) + out_shape=(SplitModelConstants.NUM_LANE_LINES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) + if 'lane_lines_prob' in outs: + self.parse_binary_crossentropy('lane_lines_prob', outs) + if 'lead_prob' in outs: + self.parse_binary_crossentropy('lead_prob', outs) + if 'lat_planner_solution' in outs: + self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) + if 'meta' in outs: + self.parse_binary_crossentropy('meta', outs) + if 'road_edges' in outs: self.parse_mdn('road_edges', outs, in_N=0, out_N=0, - out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) - self.parse_mdn('lead', outs, in_N=SplitModelConstants.LEAD_MHP_N, out_N=SplitModelConstants.LEAD_MHP_SELECTION, - out_shape=(SplitModelConstants.LEAD_TRAJ_LEN,SplitModelConstants.LEAD_WIDTH)) - if 'sim_pose' in outs: - self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) - for k in ['lead_prob', 'lane_lines_prob']: - self.parse_binary_crossentropy(k, outs) + out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) + if 'sim_pose' in outs: + self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) def parse_vision_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.WIDE_FROM_DEVICE_WIDTH,)) self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) + self.parse_dynamic_outputs(outs) self.split_outputs(outs) - self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(SplitModelConstants.DESIRE_PRED_LEN,SplitModelConstants.DESIRE_PRED_WIDTH)) - self.parse_binary_crossentropy('meta', outs) return outs def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: - self.parse_mdn('plan', outs, in_N=SplitModelConstants.PLAN_MHP_N, out_N=SplitModelConstants.PLAN_MHP_SELECTION, - out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.PLAN_WIDTH)) + self.parse_dynamic_outputs(outs) self.split_outputs(outs) - if 'lat_planner_solution' in outs: - self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) - if 'desired_curvature' in outs: - self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.DESIRED_CURV_WIDTH,)) - self.parse_categorical_crossentropy('desire_state', outs, out_shape=(SplitModelConstants.DESIRE_PRED_WIDTH,)) return outs def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: diff --git a/sunnypilot/models/README.md b/sunnypilot/models/README.md new file mode 100644 index 0000000000..bf4fb72a98 --- /dev/null +++ b/sunnypilot/models/README.md @@ -0,0 +1,63 @@ +# Model Selector Version Compatibility + +This document explains the version compatibility mechanism used by the Model Selector system, and the rationale behind certain version constraints and JSON file management strategies. + +## Overview + +The Model Selector is responsible for selecting and validating model bundles based on their metadata and version constraints. Each model bundle is distributed via a JSON file and includes a `minimumSelectorVersion` field indicating the minimum selector version required to load it. + +To ensure robust compatibility and prevent mismatches between model expectations and selector capabilities, the selector enforces two version boundaries: + +* **`REQUIRED_MIN_SELECTOR_VERSION`**: the oldest selector version we support. +* **`CURRENT_SELECTOR_VERSION`**: the current version of the selector logic. + +## Version Compatibility Check + +A model bundle is considered compatible if: + +```python +REQUIRED_MIN_SELECTOR_VERSION <= bundle["minimumSelectorVersion"] <= CURRENT_SELECTOR_VERSION +``` + +This ensures: + +* **Old bundles are rejected** if they rely on deprecated selector behavior. +* **Future bundles are ignored** if they expect logic that the current selector doesn't yet implement. + +## Handling Breaking Changes + +When a deep change in selector behavior requires *all* models to be recompiled (e.g., due to a major architectural update), we: + +1. **Create a new JSON file** (e.g., from `models_v4.json` to `models_v5.json`). +2. **Assign updated `minimumSelectorVersion` values** in the new bundles. + +This allows older selector versions to continue using the previous JSON file, while newer versions point to the new one, preventing cross-contamination. + +## Why `REQUIRED_MIN_SELECTOR_VERSION` Still Matters + +Despite using new JSON files to isolate breaking changes, `REQUIRED_MIN_SELECTOR_VERSION` plays a critical role: + +### 1. **Cached Bundle Validation** + +Model bundles are cached locally (e.g., in-memory or on disk). A user might have previously loaded a now-invalid bundle from an older JSON file. + +`REQUIRED_MIN_SELECTOR_VERSION` prevents the selector from reloading or trusting that stale cached bundle, even if the original JSON is gone. + +### 2. **Explicit Deprecation Boundary** + +By raising `REQUIRED_MIN_SELECTOR_VERSION`, we declare older bundles officially unsupported, even if they technically still exist in a legacy JSON file. + +### 3. **Avoiding Race Conditions** + +Some clients may have intermittent access to updated JSONs. The runtime check ensures version compatibility is enforced independently of external file state. + +## Summary + +| Component | Purpose | +| ------------------------------- | --------------------------------------------------------------------- | +| `minimumSelectorVersion` | Declares the minimum selector version required to load a model bundle | +| `REQUIRED_MIN_SELECTOR_VERSION` | Prevents loading bundles that are too old (e.g., from stale cache) | +| `CURRENT_SELECTOR_VERSION` | Prevents loading bundles that are too new or forward-incompatible | +| JSON file renaming | Isolates bundles by selector generation to handle full recompiles | + +This layered strategy ensures safe evolution of the model selection system while maintaining backward compatibility and runtime protection against stale or incompatible bundles. diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index 02ae52d77e..60a222a36c 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,24 +97,24 @@ 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: """Handles fetching and caching of model data from remote source""" - MODEL_URL = "https://docs.sunnypilot.ai/driving_models_v4.json" + MODEL_URL = "https://docs.sunnypilot.ai/driving_models_v7.json" def __init__(self, params: Params): self.params = params diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index a614447ceb..79241cd831 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 @@ -19,8 +18,9 @@ from openpilot.system.hardware import PC from openpilot.system.hardware.hw import Paths from pathlib import Path -CURRENT_SELECTOR_VERSION = 6 -REQUIRED_MIN_SELECTOR_VERSION = 5 +# see the README.md for more details on the model selector versioning +CURRENT_SELECTOR_VERSION = 9 +REQUIRED_MIN_SELECTOR_VERSION = 9 USE_ONNX = os.getenv('USE_ONNX', PC) @@ -63,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 @@ -110,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 630d6a8d77..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,14 +168,18 @@ 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"): + self.clear_model_cache() + self.params.remove("ModelManager_ClearCache") self._report_status() rk.keep_time() @@ -185,6 +188,31 @@ class ModelManagerSP: cloudlog.exception(f"Error in main thread: {str(e)}") rk.keep_time() + def clear_model_cache(self) -> None: + """ + Clears the model cache directory of all files except those in the active model bundle. + """ + + # Get list of files used by active model bundle + active_files = [] + if self.active_bundle is not None: # When the default model is active + for model in self.active_bundle.models: + if hasattr(model, 'artifact') and model.artifact.fileName: + active_files.append(model.artifact.fileName) + if hasattr(model, 'metadata') and model.metadata.fileName: + active_files.append(model.metadata.fileName) + + # Remove all files except active ones + model_dir = Paths.model_root() + try: + for filename in os.listdir(model_dir): + if filename not in active_files: + file_path = os.path.join(model_dir, filename) + if os.path.isfile(file_path): + os.remove(file_path) + cloudlog.info("Model cache cleared, keeping active model files") + except Exception as e: + cloudlog.exception(f"Error clearing model cache: {str(e)}") def main(): ModelManagerSP().main_thread() diff --git a/sunnypilot/models/modeld_lagd.py b/sunnypilot/models/modeld_lagd.py deleted file mode 100644 index 9e538123a8..0000000000 --- a/sunnypilot/models/modeld_lagd.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -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 -from openpilot.common.swaglog import cloudlog - - -class ModeldLagd: - def __init__(self): - self.params = Params() - - 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 - cloudlog.debug(f"LAGD USING LIVE DELAY: {lateral_delay:.3f} + {lat_smooth:.3f} = {result:.3f}") - return result - steer_actuator_delay = CP.steerActuatorDelay - lat_smooth = model.LAT_SMOOTH_SECONDS - result = (steer_actuator_delay + 0.2) + lat_smooth - cloudlog.debug(f"LAGD USING STEER ACTUATOR: {steer_actuator_delay:.3f} + 0.2 + {lat_smooth:.3f} = {result:.3f}") - return result diff --git a/sunnypilot/models/tests/model_hash b/sunnypilot/models/tests/model_hash index f65c628164..6ee27c30e6 100644 --- a/sunnypilot/models/tests/model_hash +++ b/sunnypilot/models/tests/model_hash @@ -1 +1 @@ -71979b29c4bab3007de1a4265442d79f44c0eaef066af66086dddfc432709b94 \ No newline at end of file +cee4a5f34c3c741fd67e4f130a7c21fd92258c9abfc0416c4d619d94e08a72eb \ 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/neural_network_data b/sunnypilot/neural_network_data index b59ab483c8..03cac2d30e 160000 --- a/sunnypilot/neural_network_data +++ b/sunnypilot/neural_network_data @@ -1 +1 @@ -Subproject commit b59ab483c86cd70d04de91ad878024cd66fe86b6 +Subproject commit 03cac2d30e111e0689c0429cb8c1fe6cb5a905af 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 7adde3ff42..7e0b7f5def 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/controlsd_ext.py b/sunnypilot/selfdrive/controls/controlsd_ext.py index 9a7f0cb5fc..7e06ac77c1 100644 --- a/sunnypilot/selfdrive/controls/controlsd_ext.py +++ b/sunnypilot/selfdrive/controls/controlsd_ext.py @@ -5,7 +5,7 @@ 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 cereal.messaging as messaging -from cereal import custom +from cereal import log, custom from opendbc.car import structs from openpilot.common.params import Params @@ -26,7 +26,7 @@ class ControlsExt: self.CP_SP = messaging.log_from_bytes(params.get("CarParamsSP", block=True), custom.CarParamsSP) cloudlog.info("controlsd_ext got CarParamsSP") - self.sm_services_ext = ['selfdriveStateSP'] + self.sm_services_ext = ['radarState', 'selfdriveStateSP'] self.pm_services_ext = ['carControlSP'] def get_params_sp(self) -> None: @@ -44,9 +44,32 @@ class ControlsExt: # MADS not available, use stock state to engage return bool(sm['selfdriveState'].active) + @staticmethod + def get_lead_data(ld: log.RadarState.LeadData) -> dict: + return { + "dRel": ld.dRel, + "yRel": ld.yRel, + "vRel": ld.vRel, + "aRel": ld.aRel, + "vLead": ld.vLead, + "dPath": ld.dPath, + "vLat": ld.vLat, + "vLeadK": ld.vLeadK, + "aLeadK": ld.aLeadK, + "fcw": ld.fcw, + "status": ld.status, + "aLeadTau": ld.aLeadTau, + "modelProb": ld.modelProb, + "radar": ld.radar, + "radarTrackId": ld.radarTrackId, + } + def state_control_ext(self, sm: messaging.SubMaster) -> custom.CarControlSP: CC_SP = custom.CarControlSP.new_message() + CC_SP.leadOne = self.get_lead_data(sm['radarState'].leadOne) + CC_SP.leadTwo = self.get_lead_data(sm['radarState'].leadTwo) + # MADS state CC_SP.mads = sm['selfdriveStateSP'].mads 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/dec/constants.py b/sunnypilot/selfdrive/controls/lib/dec/constants.py index 48f4203e93..4586afbc9f 100644 --- a/sunnypilot/selfdrive/controls/lib/dec/constants.py +++ b/sunnypilot/selfdrive/controls/lib/dec/constants.py @@ -1,25 +1,17 @@ class WMACConstants: - LEAD_WINDOW_SIZE = 5 - LEAD_PROB = 0.5 + # Lead detection parameters + LEAD_WINDOW_SIZE = 6 # Stable detection window + LEAD_PROB = 0.45 # Balanced threshold for lead detection - SLOW_DOWN_WINDOW_SIZE = 5 - SLOW_DOWN_PROB = 0.6 + # Slow down detection parameters + SLOW_DOWN_WINDOW_SIZE = 5 # Responsive but stable + SLOW_DOWN_PROB = 0.3 # Balanced threshold for slow down scenarios + # Optimized slow down distance curve - smooth and progressive SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.] - SLOW_DOWN_DIST = [25., 38., 55., 75., 95., 115., 130., 150.] + SLOW_DOWN_DIST = [32., 46., 64., 86., 108., 130., 145., 165.] - SLOWNESS_WINDOW_SIZE = 12 - SLOWNESS_PROB = 0.5 - SLOWNESS_CRUISE_OFFSET = 1.05 - - DANGEROUS_TTC_WINDOW_SIZE = 3 - DANGEROUS_TTC = 2.3 - - MPC_FCW_WINDOW_SIZE = 10 - MPC_FCW_PROB = 0.5 - - -class SNG_State: - off = 0 - stopped = 1 - going = 2 + # Slowness detection parameters + SLOWNESS_WINDOW_SIZE = 10 # Stable slowness detection + SLOWNESS_PROB = 0.55 # Clear threshold for slowness + SLOWNESS_CRUISE_OFFSET = 1.025 # Conservative cruise speed offset diff --git a/sunnypilot/selfdrive/controls/lib/dec/dec.py b/sunnypilot/selfdrive/controls/lib/dec/dec.py index 812e5cc15f..46cad1b048 100644 --- a/sunnypilot/selfdrive/controls/lib/dec/dec.py +++ b/sunnypilot/selfdrive/controls/lib/dec/dec.py @@ -1,88 +1,133 @@ -# The MIT License -# -# Copyright (c) 2019-, Rick Lan, dragonpilot community, and a number of other of contributors. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# Version = 2025-1-18 +""" +Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors. -import numpy as np +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. +""" +# Version = 2025-6-30 from cereal import messaging from opendbc.car import structs from numpy import interp from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL -from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants, SNG_State +from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants +from typing import Literal # d-e2e, from modeldata.h TRAJECTORY_SIZE = 33 +SET_MODE_TIMEOUT = 15 -HIGHWAY_CRUISE_KPH = 70 - -STOP_AND_GO_FRAME = 60 - -SET_MODE_TIMEOUT = 10 - -V_ACC_MIN = 9.72 +# Define the valid mode types +ModeType = Literal['acc', 'blended'] -class GenericMovingAverageCalculator: - def __init__(self, window_size): - self.window_size = window_size - self.data = [] - self.total = 0 +class SmoothKalmanFilter: + """Enhanced Kalman filter with smoothing for stable decision making.""" - def add_data(self, value: float) -> None: - if len(self.data) == self.window_size: - self.total -= self.data.pop(0) - self.data.append(value) - self.total += value + def __init__(self, initial_value=0, measurement_noise=0.1, process_noise=0.01, + alpha=1.0, smoothing_factor=0.85): + self.x = initial_value + self.P = 1.0 + self.R = measurement_noise + self.Q = process_noise + self.alpha = alpha + self.smoothing_factor = smoothing_factor + self.initialized = False + self.history = [] + self.max_history = 10 + self.confidence = 0.0 - def get_moving_average(self) -> float | None: - return None if len(self.data) == 0 else self.total / len(self.data) + def add_data(self, measurement): + if len(self.history) >= self.max_history: + self.history.pop(0) + self.history.append(measurement) - def reset_data(self) -> None: - self.data = [] - self.total = 0 + if not self.initialized: + self.x = measurement + self.initialized = True + self.confidence = 0.1 + return + + self.P = self.alpha * self.P + self.Q + + K = self.P / (self.P + self.R) + effective_K = K * (1.0 - self.smoothing_factor) + self.smoothing_factor * 0.1 + + innovation = measurement - self.x + self.x = self.x + effective_K * innovation + self.P = (1 - effective_K) * self.P + + if abs(innovation) < 0.1: + self.confidence = min(1.0, self.confidence + 0.05) + else: + self.confidence = max(0.1, self.confidence - 0.02) + + def get_value(self): + return self.x if self.initialized else None + + def get_confidence(self): + return self.confidence + + def reset_data(self): + self.initialized = False + self.history = [] + self.confidence = 0.0 -class WeightedMovingAverageCalculator: - def __init__(self, window_size): - self.window_size = window_size - self.data = [] - self.weights = np.linspace(1, 3, window_size) # Linear weights, adjust as needed +class ModeTransitionManager: + """Manages smooth transitions between driving modes with hysteresis.""" - def add_data(self, value: float) -> None: - if len(self.data) == self.window_size: - self.data.pop(0) - self.data.append(value) + def __init__(self): + self.current_mode: ModeType = 'acc' + self.mode_confidence = {'acc': 1.0, 'blended': 0.0} + self.transition_timeout = 0 + self.min_mode_duration = 10 + self.mode_duration = 0 + self.emergency_override = False - def get_weighted_average(self) -> float | None: - if len(self.data) == 0: - return None - weighted_sum: float = float(np.dot(self.data, self.weights[-len(self.data):])) - weight_total: float = float(np.sum(self.weights[-len(self.data):])) - return weighted_sum / weight_total + def request_mode(self, mode: ModeType, confidence: float = 1.0, emergency: bool = False): + # Emergency override for critical situations (stops, collisions) + if emergency: + self.emergency_override = True + self.current_mode = mode + self.transition_timeout = SET_MODE_TIMEOUT + self.mode_duration = 0 + return - def reset_data(self) -> None: - self.data = [] + self.mode_confidence[mode] = min(1.0, self.mode_confidence[mode] + 0.1 * confidence) + for m in self.mode_confidence: + if m != mode: + self.mode_confidence[m] = max(0.0, self.mode_confidence[m] - 0.05) + + # Require minimum duration in current mode (unless emergency) + if self.mode_duration < self.min_mode_duration and not self.emergency_override: + return + + # Hysteresis: higher threshold for mode changes + confidence_threshold = 0.6 if mode != self.current_mode else 0.3 # Lower threshold for faster response + + if self.mode_confidence[mode] > confidence_threshold: + if mode != self.current_mode and self.transition_timeout == 0: + self.transition_timeout = SET_MODE_TIMEOUT + self.current_mode = mode + self.mode_duration = 0 + + def update(self): + if self.transition_timeout > 0: + self.transition_timeout -= 1 + self.mode_duration += 1 + + # Reset emergency override after some time + if self.emergency_override and self.mode_duration > 20: + self.emergency_override = False + + # Gradual confidence decay + for mode in self.mode_confidence: + self.mode_confidence[mode] *= 0.98 + + def get_mode(self) -> ModeType: + return self.current_mode class DynamicExperimentalController: @@ -92,51 +137,59 @@ class DynamicExperimentalController: self._params = params or Params() self._enabled: bool = self._params.get_bool("DynamicExperimentalControl") self._active: bool = False - self._mode: str = 'acc' self._frame: int = 0 + self._urgency = 0.0 - # Use weighted moving average for filtering leads - self._lead_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.LEAD_WINDOW_SIZE) + self._mode_manager = ModeTransitionManager() + + # Smooth filters for stable decision making with faster response for critical scenarios + self._lead_filter = SmoothKalmanFilter( + measurement_noise=0.15, + process_noise=0.05, + alpha=1.02, + smoothing_factor=0.8 + ) + + self._slow_down_filter = SmoothKalmanFilter( + measurement_noise=0.1, + process_noise=0.1, + alpha=1.05, + smoothing_factor=0.7 + ) + + self._slowness_filter = SmoothKalmanFilter( + measurement_noise=0.1, + process_noise=0.06, + alpha=1.015, + smoothing_factor=0.92 + ) + + self._mpc_fcw_filter = SmoothKalmanFilter( + measurement_noise=0.2, + process_noise=0.1, + alpha=1.1, + smoothing_factor=0.5 + ) self._has_lead_filtered = False - self._has_lead_filtered_prev = False - - self._slow_down_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.SLOW_DOWN_WINDOW_SIZE) - self._has_slow_down: bool = False - self._slow_down_confidence: float = 0.0 - - self._has_blinkers = False - - self._slowness_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.SLOWNESS_WINDOW_SIZE) - self._has_slowness: bool = False - - self._has_nav_instruction = False - - self._dangerous_ttc_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.DANGEROUS_TTC_WINDOW_SIZE) - self._has_dangerous_ttc: bool = False - - self._v_ego_kph = 0. - self._v_cruise_kph = 0. - - self._has_lead = False - + self._has_slow_down = False + self._has_slowness = False + self._has_mpc_fcw = False + self._v_ego_kph = 0.0 + self._v_cruise_kph = 0.0 self._has_standstill = False - self._has_standstill_prev = False - - self._sng_transit_frame = 0 - self._sng_state = SNG_State.off - - self._mpc_fcw_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.MPC_FCW_WINDOW_SIZE) - self._has_mpc_fcw: bool = False self._mpc_fcw_crash_cnt = 0 - - self._set_mode_timeout = 0 + self._standstill_count = 0 + # debug + self._endpoint_x = float('inf') + self._expected_distance = 0.0 + self._trajectory_valid = False def _read_params(self) -> None: if self._frame % int(1. / DT_MDL) == 0: self._enabled = self._params.get_bool("DynamicExperimentalControl") def mode(self) -> str: - return str(self._mode) + return self._mode_manager.get_mode() def enabled(self) -> bool: return self._enabled @@ -144,46 +197,9 @@ class DynamicExperimentalController: def active(self) -> bool: return self._active - @staticmethod - def _anomaly_detection(recent_data: list[float], threshold: float = 2.0, context_check: bool = True) -> bool: - """ - Basic anomaly detection using standard deviation. - """ - if len(recent_data) < 5: - return False - mean: float = float(np.mean(recent_data)) - std_dev: float = float(np.std(recent_data)) - anomaly: bool = bool(recent_data[-1] > mean + threshold * std_dev) - - # Context check to ensure repeated anomaly - if context_check: - return np.count_nonzero(np.array(recent_data) > mean + threshold * std_dev) > 1 - return anomaly - - def _adaptive_slowdown_threshold(self) -> float: - """ - Adapts the slow-down threshold based on vehicle speed and recent behavior. - """ - slowdown_scaling_factor: float = (1.0 + 0.03 * np.log(1 + len(self._slow_down_gmac.data))) - adaptive_threshold: float = float( - interp(self._v_ego_kph, WMACConstants.SLOW_DOWN_BP, WMACConstants.SLOW_DOWN_DIST) * slowdown_scaling_factor - ) - return adaptive_threshold - - def _smoothed_lead_detection(self, lead_prob: float, smoothing_factor: float = 0.2): - """ - Smoothing the lead detection to avoid erratic behavior. - """ - lead_filtering: float = (1 - smoothing_factor) * self._has_lead_filtered + smoothing_factor * lead_prob - return lead_filtering > WMACConstants.LEAD_PROB - - def _adaptive_lead_prob_threshold(self) -> float: - """ - Adapts lead probability threshold based on driving conditions. - """ - if self._v_ego_kph > HIGHWAY_CRUISE_KPH: - return float(WMACConstants.LEAD_PROB + 0.1) # Increase the threshold on highways - return float(WMACConstants.LEAD_PROB) + def set_mpc_fcw_crash_cnt(self) -> None: + """Set MPC FCW crash count""" + self._mpc_fcw_crash_cnt = self._mpc.crash_cnt def _update_calculations(self, sm: messaging.SubMaster) -> None: car_state = sm['carState'] @@ -192,186 +208,168 @@ class DynamicExperimentalController: self._v_ego_kph = car_state.vEgo * 3.6 self._v_cruise_kph = car_state.vCruise - self._has_lead = lead_one.status self._has_standstill = car_state.standstill - # fcw detection - self._mpc_fcw_gmac.add_data(self._mpc_fcw_crash_cnt > 0) - if _mpc_fcw_weighted_average := self._mpc_fcw_gmac.get_weighted_average(): - self._has_mpc_fcw = _mpc_fcw_weighted_average > WMACConstants.MPC_FCW_PROB - else: - self._has_mpc_fcw = False - - # nav enable detection - # self._has_nav_instruction = md.navEnabledDEPRECATED and maneuver_distance / max(car_state.vEgo, 1) < 13 - - # lead detection with smoothing - self._lead_gmac.add_data(lead_one.status) - self._has_lead_filtered = (self._lead_gmac.get_weighted_average() or -1.) > WMACConstants.LEAD_PROB - #lead_prob = self._lead_gmac.get_weighted_average() or 0 - #self._has_lead_filtered = self._smoothed_lead_detection(lead_prob) - - # adaptive slow down detection - adaptive_threshold = self._adaptive_slowdown_threshold() - slow_down_trigger = len(md.orientation.x) == len(md.position.x) == TRAJECTORY_SIZE and md.position.x[TRAJECTORY_SIZE - 1] < adaptive_threshold - self._slow_down_gmac.add_data(slow_down_trigger) - if _has_slow_down_weighted_average := self._slow_down_gmac.get_weighted_average(): - self._has_slow_down = _has_slow_down_weighted_average > WMACConstants.SLOW_DOWN_PROB - self._slow_down_confidence = _has_slow_down_weighted_average # Store confidence level - else: - self._has_slow_down = False - self._slow_down_confidence = 0.0 # No confidence if no slowdown - - # anomaly detection for slow down events - if self._anomaly_detection(self._slow_down_gmac.data): - self._slow_down_confidence *= 0.85 # Reduce confidence - self._has_slow_down = self._slow_down_confidence > WMACConstants.SLOW_DOWN_PROB - - # blinker detection - self._has_blinkers = car_state.leftBlinker or car_state.rightBlinker - - # sng detection + # standstill detection if self._has_standstill: - self._sng_state = SNG_State.stopped - self._sng_transit_frame = 0 + self._standstill_count = min(20, self._standstill_count + 1) else: - if self._sng_transit_frame == 0: - if self._sng_state == SNG_State.stopped: - self._sng_state = SNG_State.going - self._sng_transit_frame = STOP_AND_GO_FRAME - elif self._sng_state == SNG_State.going: - self._sng_state = SNG_State.off - elif self._sng_transit_frame > 0: - self._sng_transit_frame -= 1 + self._standstill_count = max(0, self._standstill_count - 1) - # slowness detection - if not self._has_standstill: - self._slowness_gmac.add_data(self._v_ego_kph <= (self._v_cruise_kph * WMACConstants.SLOWNESS_CRUISE_OFFSET)) - if _slowness_weighted_average := self._slowness_gmac.get_weighted_average(): - self._has_slowness = _slowness_weighted_average > WMACConstants.SLOWNESS_PROB - else: - self._has_slowness = False + # Lead detection + self._lead_filter.add_data(float(lead_one.status)) + lead_value = self._lead_filter.get_value() or 0.0 + self._has_lead_filtered = lead_value > WMACConstants.LEAD_PROB - # dangerous TTC detection - if not self._has_lead_filtered and self._has_lead_filtered_prev: - self._dangerous_ttc_gmac.reset_data() - self._has_dangerous_ttc = False + # MPC FCW detection + fcw_filtered_value = self._mpc_fcw_filter.get_value() or 0.0 + self._mpc_fcw_filter.add_data(float(self._mpc_fcw_crash_cnt > 0)) + self._has_mpc_fcw = fcw_filtered_value > 0.5 - if self._has_lead and car_state.vEgo >= 0.01: - self._dangerous_ttc_gmac.add_data(lead_one.dRel / car_state.vEgo) + # Slow down detection + self._calculate_slow_down(md) - if _dangerous_ttc_weighted_average := self._dangerous_ttc_gmac.get_weighted_average(): - self._has_dangerous_ttc = _dangerous_ttc_weighted_average <= WMACConstants.DANGEROUS_TTC - else: - self._has_dangerous_ttc = False + # Slowness detection + if not (self._standstill_count > 5) and not self._has_slow_down: + current_slowness = float(self._v_ego_kph <= (self._v_cruise_kph * WMACConstants.SLOWNESS_CRUISE_OFFSET)) + self._slowness_filter.add_data(current_slowness) + slowness_value = self._slowness_filter.get_value() or 0.0 - # keep prev values - self._has_standstill_prev = self._has_standstill - self._has_lead_filtered_prev = self._has_lead_filtered + # Hysteresis for slowness + threshold = WMACConstants.SLOWNESS_PROB * (0.8 if self._has_slowness else 1.1) + self._has_slowness = slowness_value > threshold + + def _calculate_slow_down(self, md): + """Calculate urgency based on trajectory endpoint vs expected distance.""" + + # Reset to safe defaults + urgency = 0.0 + self._endpoint_x = float('inf') + self._trajectory_valid = False + + #Require exact trajectory size + position_valid = len(md.position.x) == TRAJECTORY_SIZE + orientation_valid = len(md.orientation.x) == TRAJECTORY_SIZE + + if not (position_valid and orientation_valid): + # Invalid trajectory - this itself might indicate a stop scenario + # Apply moderate urgency for incomplete trajectories at speed + if self._v_ego_kph > 20.0: + urgency = 0.3 + + self._slow_down_filter.add_data(urgency) + urgency_filtered = self._slow_down_filter.get_value() or 0.0 + self._has_slow_down = urgency_filtered > WMACConstants.SLOW_DOWN_PROB + self._urgency = urgency_filtered + return + + # We have a valid full trajectory + self._trajectory_valid = True + + # Use the exact endpoint (33rd point, index 32) + endpoint_x = md.position.x[TRAJECTORY_SIZE - 1] + self._endpoint_x = endpoint_x + + # Get expected distance based on current speed using tuned constants + expected_distance = interp(self._v_ego_kph, + WMACConstants.SLOW_DOWN_BP, + WMACConstants.SLOW_DOWN_DIST) + self._expected_distance = expected_distance + + # Calculate urgency based on trajectory shortage + if endpoint_x < expected_distance: + shortage = expected_distance - endpoint_x + shortage_ratio = shortage / expected_distance + + # Base urgency on shortage ratio + urgency = min(1.0, shortage_ratio * 2.0) + + # Increase urgency for very short trajectories (imminent stops) + critical_distance = expected_distance * 0.3 + if endpoint_x < critical_distance: + urgency = min(1.0, urgency * 2.0) + + # Speed-based urgency adjustment + if self._v_ego_kph > 25.0: + speed_factor = 1.0 + (self._v_ego_kph - 25.0) / 80.0 + urgency = min(1.0, urgency * speed_factor) + + # Apply filtering but with less smoothing for stops + self._slow_down_filter.add_data(urgency) + urgency_filtered = self._slow_down_filter.get_value() or 0.0 + + # Update state with lower threshold for better stop detection + self._has_slow_down = urgency_filtered > (WMACConstants.SLOW_DOWN_PROB * 0.8) + self._urgency = urgency_filtered def _radarless_mode(self) -> None: - # when mpc fcw crash prob is high - # use blended to slow down quickly + """Radarless mode decision logic with emergency handling.""" + + # EMERGENCY: MPC FCW - immediate blended mode if self._has_mpc_fcw: - self._set_mode('blended') + self._mode_manager.request_mode('blended', confidence=1.0, emergency=True) return - # Nav enabled and distance to upcoming turning is 300 or below - # if self._has_nav_instruction: - # self._set_mode('blended') - # return - - # when blinker is on and speed is driving below V_ACC_MIN: blended - # we don't want it to switch mode at higher speed, blended may trigger hard brake - # if self._has_blinkers and self._v_ego_kph < V_ACC_MIN: - # self._set_mode('blended') - # return - - # when at highway cruise and SNG: blended - # ensuring blended mode is used because acc is bad at catching SNG lead car - # especially those who accel very fast and then brake very hard. - # if self._sng_state == SNG_State.going and self._v_cruise_kph >= V_ACC_MIN: - # self._set_mode('blended') - # return - - # when standstill: blended - # in case of lead car suddenly move away under traffic light, acc mode won't brake at traffic light. - if self._has_standstill: - self._set_mode('blended') + # Standstill: use blended + if self._standstill_count > 3: + self._mode_manager.request_mode('blended', confidence=0.9) return - # when detecting slow down scenario: blended - # e.g. traffic light, curve, stop sign etc. + # Slow down scenarios: emergency for high urgency, normal for lower urgency if self._has_slow_down: - self._set_mode('blended') + if self._urgency > 0.7: + # Emergency: immediate blended mode for high urgency stops + self._mode_manager.request_mode('blended', confidence=1.0, emergency=True) + else: + # Normal: blended with urgency-based confidence + confidence = min(1.0, self._urgency * 1.5) + self._mode_manager.request_mode('blended', confidence=confidence) return - # when detecting lead slow down: blended - # use blended for higher braking capability - if self._has_dangerous_ttc: - self._set_mode('blended') + # Driving slow: use ACC (but not if actively slowing down) + if self._has_slowness and not self._has_slow_down: + self._mode_manager.request_mode('acc', confidence=0.8) return - # car driving at speed lower than set speed: acc - if self._has_slowness: - self._set_mode('acc') - return - - self._set_mode('acc') + # Default: ACC + self._mode_manager.request_mode('acc', confidence=0.7) def _radar_mode(self) -> None: - # when mpc fcw crash prob is high - # use blended to slow down quickly + """Radar mode with emergency handling.""" + + # EMERGENCY: MPC FCW - immediate blended mode if self._has_mpc_fcw: - self._set_mode('blended') + self._mode_manager.request_mode('blended', confidence=1.0, emergency=True) return - # If there is a filtered lead, the vehicle is not in standstill, and the lead vehicle's yRel meets the condition, - if self._has_lead_filtered and not self._has_standstill: - self._set_mode('acc') + # If lead detected and not in standstill: always use ACC + if self._has_lead_filtered and not (self._standstill_count > 3): + self._mode_manager.request_mode('acc', confidence=1.0) return - # when blinker is on and speed is driving below V_ACC_MIN: blended - # we don't want it to switch mode at higher speed, blended may trigger hard brake - # if self._has_blinkers and self._v_ego_kph < V_ACC_MIN: - # self._set_mode('blended') - # return - - # when standstill: blended - # in case of lead car suddenly move away under traffic light, acc mode won't brake at traffic light. - if self._has_standstill: - self._set_mode('blended') - return - - # when detecting slow down scenario: blended - # e.g. traffic light, curve, stop sign etc. + # Slow down scenarios: emergency for high urgency, normal for lower urgency if self._has_slow_down: - self._set_mode('blended') + if self._urgency > 0.7: + # Emergency: immediate blended mode for high urgency stops + self._mode_manager.request_mode('blended', confidence=1.0, emergency=True) + else: + # Normal: blended with urgency-based confidence + confidence = min(1.0, self._urgency * 1.3) + self._mode_manager.request_mode('blended', confidence=confidence) return - # car driving at speed lower than set speed: acc - if self._has_slowness: - self._set_mode('acc') + # Standstill: use blended + if self._standstill_count > 3: + self._mode_manager.request_mode('blended', confidence=0.9) return - # Nav enabled and distance to upcoming turning is 300 or below - # if self._has_nav_instruction: - # self._set_mode('blended') - # return + # Driving slow: use ACC (but not if actively slowing down) + if self._has_slowness and not self._has_slow_down: + self._mode_manager.request_mode('acc', confidence=0.8) + return - self._set_mode('acc') - - def set_mpc_fcw_crash_cnt(self) -> None: - self._mpc_fcw_crash_cnt = self._mpc.crash_cnt - - def _set_mode(self, mode: str) -> None: - if self._set_mode_timeout == 0: - self._mode = mode - if mode == 'blended': - self._set_mode_timeout = SET_MODE_TIMEOUT - - if self._set_mode_timeout > 0: - self._set_mode_timeout -= 1 + # Default: ACC + self._mode_manager.request_mode('acc', confidence=0.7) def update(self, sm: messaging.SubMaster) -> None: self._read_params() @@ -385,6 +383,6 @@ class DynamicExperimentalController: else: self._radar_mode() + self._mode_manager.update() self._active = sm['selfdriveState'].experimentalMode and self._enabled - self._frame += 1 diff --git a/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py b/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py index d15d88aefe..f9da39c03b 100644 --- a/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py +++ b/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py @@ -1,248 +1,94 @@ import pytest -import numpy as np -from openpilot.common.params import Params -from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants, SNG_State -from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController, TRAJECTORY_SIZE, STOP_AND_GO_FRAME - -class MockInterp: - def __call__(self, x, xp, fp): - return np.interp(x, xp, fp) - -class MockCarState: - def __init__(self, v_ego=0., standstill=False, left_blinker=False, right_blinker=False): - self.vEgo = v_ego - self.standstill = standstill - self.leftBlinker = left_blinker - self.rightBlinker = right_blinker +from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController class MockLeadOne: - def __init__(self, status=False, d_rel=0): + def __init__(self, status=0.0): self.status = status - self.dRel = d_rel + +class MockRadarState: + def __init__(self, status=0.0): + self.leadOne = MockLeadOne(status=status) + +class MockCarState: + def __init__(self, vEgo=0.0, vCruise=0.0, standstill=False): + self.vEgo = vEgo + self.vCruise = vCruise + self.standstill = standstill class MockModelData: - def __init__(self, x_vals=None, positions=None): - self.orientation = type('Orientation', (), {'x': x_vals})() - self.position = type('Position', (), {'x': positions})() + def __init__(self, valid=True): + size = 33 if valid else 10 # incomplete if invalid + self.position = type("Pos", (), {"x": [0.0] * size})() + self.orientation = type("Ori", (), {"x": [0.0] * size})() -class MockControlState: - def __init__(self, v_cruise=0): - self.vCruise = v_cruise +class MockSelfDriveState: + def __init__(self, experimentalMode=False): + self.experimentalMode = experimentalMode + +class MockParams: + def get_bool(self, name): + return True @pytest.fixture -def interp(monkeypatch): - mock_interp = MockInterp() - monkeypatch.setattr('numpy.interp', mock_interp) - return mock_interp +def default_sm(): + sm = { + 'carState': MockCarState(vEgo=10.0, vCruise=20.0), + 'radarState': MockRadarState(status=1.0), + 'modelV2': MockModelData(valid=True), + 'selfdriveState': MockSelfDriveState(experimentalMode=True), + } + return sm @pytest.fixture -def controller(interp): - params = Params() - params.put_bool("DynamicExperimentalControl", True) - return DynamicExperimentalController() +def mock_cp(): + class CP: + radarUnavailable = False + return CP() -def test_initial_state(controller): - """Test initial state of the controller""" - assert controller._mode == 'acc' - assert not controller._has_lead - assert not controller._has_standstill - assert controller._sng_state == SNG_State.off - assert not controller._has_lead_filtered - assert not controller._has_slow_down - assert not controller._has_dangerous_ttc - assert not controller._has_mpc_fcw +@pytest.fixture +def mock_mpc(): + class MPC: + crash_cnt = 0 + return MPC() -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_standstill_detection(controller, has_radar): - """Test standstill detection and state transitions""" - car_state = MockCarState(standstill=True) - lead_one = MockLeadOne() - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState() +# Fake Kalman Filter that always returns a given value +class FakeKalman: + def __init__(self, value=1.0): + self.value = value + def add_data(self, v): pass + def get_value(self): return self.value + def get_confidence(self): return 1.0 + def reset_data(self): pass - # Test transition to standstill - controller.update(not has_radar, car_state, lead_one, md, controls_state) - assert controller._sng_state == SNG_State.stopped - assert controller.get_mpc_mode() == 'blended' +def test_initial_mode_is_acc(mock_cp, mock_mpc): + controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) + assert controller.mode() == "acc" - # Test transition from standstill to moving - car_state.standstill = False - controller.update(not has_radar, car_state, lead_one, md, controls_state) - assert controller._sng_state == SNG_State.going +def test_standstill_triggers_blended(mock_cp, mock_mpc, default_sm): + controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) + default_sm['carState'].standstill = True + for _ in range(10): + controller.update(default_sm) + assert controller.mode() == "blended" - # Test complete transition to normal driving - for _ in range(STOP_AND_GO_FRAME + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - assert controller._sng_state == SNG_State.off +def test_emergency_blended_on_fcw(mock_cp, mock_mpc, default_sm): + controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) + mock_mpc.crash_cnt = 1 # simulate FCW + for _ in range(2): + controller.update(default_sm) + assert controller.mode() == "blended" -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_lead_detection(controller, has_radar): - """Test lead vehicle detection and filtering""" - car_state = MockCarState(v_ego=20) # 72 kph - lead_one = MockLeadOne(status=True, d_rel=50) # Safe distance - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=72) +def test_radarless_slowdown_triggers_blended(mock_cp, mock_mpc, default_sm): + mock_cp.radarUnavailable = True + controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) - # Let moving average stabilize - for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) + # Force conditions to simulate slowdown + controller._slow_down_filter = FakeKalman(value=1.0) # Ensure urgency triggers slowdown + controller._v_ego_kph = 35.0 + default_sm['modelV2'] = MockModelData(valid=False) # Incomplete trajectory - assert controller._has_lead_filtered - expected_mode = 'acc' if has_radar else 'blended' - assert controller.get_mpc_mode() == expected_mode + for _ in range(3): + controller.update(default_sm) - # Test lead loss detection - lead_one.status = False - for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert not controller._has_lead_filtered - -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_slow_down_detection(controller, has_radar): - """Test slow down detection based on trajectory""" - car_state = MockCarState(v_ego=10/3.6) # 10 kph - lead_one = MockLeadOne() - x_vals = [0] * TRAJECTORY_SIZE - positions = [20] * TRAJECTORY_SIZE # Position within slow down threshold - md = MockModelData(x_vals=x_vals, positions=positions) - controls_state = MockControlState(v_cruise=30) - - # Test slow down detection - for _ in range(WMACConstants.SLOW_DOWN_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_slow_down - assert controller.get_mpc_mode() == 'blended' - - # Test slow down recovery - positions = [200] * TRAJECTORY_SIZE # Position outside slow down threshold - md = MockModelData(x_vals=x_vals, positions=positions) - for _ in range(WMACConstants.SLOW_DOWN_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert not controller._has_slow_down - -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_dangerous_ttc_detection(controller, has_radar): - """Test Time-To-Collision detection and handling""" - car_state = MockCarState(v_ego=10) # 36 kph - lead_one = MockLeadOne(status=True) - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=36) - - # First establish normal conditions with lead - lead_one.dRel = 100 # Safe distance - for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): # First establish lead detection - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_lead_filtered # Verify lead is detected - - # Now test dangerous TTC detection - lead_one.dRel = 10 # 10m distance - should trigger dangerous TTC - # TTC = dRel/vEgo = 10/10 = 1s (which is less than DANGEROUS_TTC = 2.3s) - - # Need to update multiple times to allow the weighted average to stabilize - for _ in range(WMACConstants.DANGEROUS_TTC_WINDOW_SIZE * 2): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_dangerous_ttc, "TTC of 1s should be considered dangerous" - expected_mode = 'acc' if has_radar else 'blended' - assert controller.get_mpc_mode() == expected_mode, f"Should be in [{expected_mode}] mode with dangerous TTC" - -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_mode_transitions(controller, has_radar): - """Test comprehensive mode transitions under different conditions""" - # Initialize with normal driving conditions - car_state = MockCarState(v_ego=25) # 90 kph - lead_one = MockLeadOne(status=False) - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[200] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=100) - - def stabilize_filters(): - """Helper to let all moving averages stabilize""" - for _ in range(max(WMACConstants.LEAD_WINDOW_SIZE, WMACConstants.SLOW_DOWN_WINDOW_SIZE, - WMACConstants.DANGEROUS_TTC_WINDOW_SIZE, WMACConstants.MPC_FCW_WINDOW_SIZE) + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - # Test 1: Normal driving -> ACC mode - stabilize_filters() - assert controller.get_mpc_mode() == 'acc', "Should be in ACC mode under normal driving conditions" - - # Test 2: Standstill -> Blended mode - car_state.standstill = True - controller.update(not has_radar, car_state, lead_one, md, controls_state) - assert controller.get_mpc_mode() == 'blended', "Should be in blended mode during standstill" - - # Test 3: Lead car appears -> ACC mode - car_state = MockCarState(v_ego=20) # Reset car state - lead_one.status = True - lead_one.dRel = 50 # Safe distance - stabilize_filters() - assert not controller._has_dangerous_ttc, "Should not have dangerous TTC" - assert controller.get_mpc_mode() == 'acc', "Should be in ACC mode with safe lead distance" - - # Test 4: Dangerous TTC -> Blended mode - car_state = MockCarState(v_ego=20) # 72 kph - lead_one.status = True - lead_one.dRel = 50 # First establish normal lead detection - - # First establish lead detection - for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_lead_filtered # Verify lead is detected - - # Now create dangerous TTC condition - lead_one.dRel = 20 # This creates a TTC of 1s, well below DANGEROUS_TTC - - for _ in range(WMACConstants.DANGEROUS_TTC_WINDOW_SIZE * 2): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_dangerous_ttc, "Should detect dangerous TTC condition" - expected_mode = 'acc' if has_radar else 'blended' - assert controller.get_mpc_mode() == expected_mode, f"Should be in [{expected_mode}] mode with dangerous TTC" - -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_mpc_fcw_handling(controller, has_radar): - """Test MPC FCW crash count handling and mode transitions""" - car_state = MockCarState(v_ego=20) - lead_one = MockLeadOne() - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=72) - - # Test FCW activation - controller.set_mpc_fcw_crash_cnt(5) - for _ in range(WMACConstants.MPC_FCW_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_mpc_fcw - assert controller.get_mpc_mode() == 'blended' - - # Test FCW recovery - controller.set_mpc_fcw_crash_cnt(0) - for _ in range(WMACConstants.MPC_FCW_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert not controller._has_mpc_fcw - -def test_radar_unavailable_handling(controller): - """Test behavior transitions between radar available and unavailable states""" - car_state = MockCarState(v_ego=27.78) # 100 kph - lead_one = MockLeadOne(status=True, d_rel=50) - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=100) - - # Test with radar available - for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): - controller.update(False, car_state, lead_one, md, controls_state) - radar_mode = controller.get_mpc_mode() - - # Test with radar unavailable - for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): - controller.update(True, car_state, lead_one, md, controls_state) - radarless_mode = controller.get_mpc_mode() - - assert radar_mode is not None - assert radarless_mode is not None + assert controller.mode() == "blended" diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py index 8ccd81d8ff..644f28573a 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py @@ -11,7 +11,8 @@ from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from openpilot.selfdrive.modeld.constants import ModelConstants LAT_PLAN_MIN_IDX = 5 -LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan +LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan + def get_predicted_lateral_jerk(lat_accels, t_diffs): # compute finite difference between subsequent model_v2.acceleration.y values @@ -45,7 +46,7 @@ class LatControlTorqueExtBase: def __init__(self, lac_torque, CP, CP_SP): self.model_v2 = None self.model_valid = False - self.use_steering_angle = lac_torque.use_steering_angle + self.torque_params = lac_torque.torque_params self.actual_lateral_jerk: float = 0.0 self.lateral_jerk_setpoint: float = 0.0 @@ -53,7 +54,6 @@ class LatControlTorqueExtBase: self.lookahead_lateral_jerk: float = 0.0 self.torque_from_lateral_accel = lac_torque.torque_from_lateral_accel - self.torque_params = lac_torque.torque_params self._ff = 0.0 self._pid_log = None @@ -106,9 +106,8 @@ class LatControlTorqueExtBase: self.lateral_jerk_measurement = 0.0 self.lookahead_lateral_jerk = 0.0 - if self.use_steering_angle: - actual_curvature_rate = -VM.calc_curvature(math.radians(CS.steeringRateDeg), CS.vEgo, 0.0) - self.actual_lateral_jerk = actual_curvature_rate * CS.vEgo ** 2 + actual_curvature_rate = -VM.calc_curvature(math.radians(CS.steeringRateDeg), CS.vEgo, 0.0) + self.actual_lateral_jerk = actual_curvature_rate * CS.vEgo ** 2 if self.model_valid: # prepare "look-ahead" desired lateral jerk @@ -118,8 +117,7 @@ class LatControlTorqueExtBase: desired_lateral_jerk = (np.interp(self.desired_lat_jerk_time, ModelConstants.T_IDXS, self.model_v2.acceleration.y) - desired_lateral_accel) / self.desired_lat_jerk_time self.lookahead_lateral_jerk = get_lookahead_value(predicted_lateral_jerk[LAT_PLAN_MIN_IDX:friction_upper_idx], desired_lateral_jerk) - if not self.use_steering_angle or self.lookahead_lateral_jerk == 0.0: - self.lookahead_lateral_jerk = 0.0 + if self.lookahead_lateral_jerk == 0.0: self.actual_lateral_jerk = 0.0 self.lat_accel_friction_factor = 1.0 self.lateral_jerk_setpoint = self.lat_jerk_friction_factor * self.lookahead_lateral_jerk diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 56f32373d5..6952a97f1f 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -8,6 +8,7 @@ See the LICENSE.md file in the root directory for more details. from cereal import messaging, custom from opendbc.car import structs from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController +from openpilot.sunnypilot.models.helpers import get_active_bundle DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState @@ -15,6 +16,12 @@ DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimen class LongitudinalPlannerSP: def __init__(self, CP: structs.CarParams, mpc): self.dec = DynamicExperimentalController(CP, mpc) + self.generation = int(model_bundle.generation) if (model_bundle := get_active_bundle()) else None + + @property + def mlsim(self) -> bool: + # If we don't have a generation set, we assume it's default model. Which as of today are mlsim. + return bool(self.generation is None or self.generation >= 11) def get_mpc_mode(self) -> str | None: if not self.dec.active(): 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 b2701bc010..fae221bb74 100644 --- a/sunnypilot/selfdrive/controls/lib/param_store.py +++ b/sunnypilot/selfdrive/controls/lib/param_store.py @@ -22,14 +22,16 @@ class ParamStore: self.keys = universal_params + brand_params self.values = {} + self.cached_params_list: list[capnp.lib.capnp._DynamicStructBuilder] | None = None def update(self, params: Params) -> None: - self.values = {k: params.get(k, encoding='utf8') or "0" for k in self.keys} + old_values = dict(self.values) + self.values = {k: params.get(k) or "0" for k in self.keys} + if old_values != self.values: + self.cached_params_list = None def publish(self) -> list[capnp.lib.capnp._DynamicStructBuilder]: - params_list: list[capnp.lib.capnp._DynamicStructBuilder] = [] - - for k in self.keys: - params_list.append(custom.CarControlSP.Param(key=k, value=self.values[k])) - - return params_list + if self.cached_params_list is None: + # TODO-SP: Why are we doing a list instead of a dictionary here? + self.cached_params_list = [custom.CarControlSP.Param(key=k, value=self.values[k]) for k in self.keys] + return self.cached_params_list 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..90eae1dfe8 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -15,7 +15,7 @@ from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity from openpilot.common.swaglog import cloudlog from openpilot.system.athena.athenad import ws_send, jsonrpc_handler, \ - recv_queue, UploadQueueCache, upload_queue, cur_upload_items, backoff, ws_manage, log_handler, start_local_proxy_shim + recv_queue, UploadQueueCache, upload_queue, cur_upload_items, backoff, ws_manage, log_handler, start_local_proxy_shim, upload_handler from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException, create_connection) @@ -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) @@ -47,7 +47,7 @@ def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None: threading.Thread(target=ws_send, args=(ws, end_event), name='ws_send'), threading.Thread(target=ws_ping, args=(ws, end_event), name='ws_ping'), threading.Thread(target=ws_queue, args=(end_event,), name='ws_queue'), - # threading.Thread(target=upload_handler, args=(end_event,), name='upload_handler'), + threading.Thread(target=upload_handler, args=(end_event,), name='upload_handler'), # threading.Thread(target=sunny_log_handler, args=(end_event, comma_prime_cellular_end_event), name='log_handler'), # threading.Thread(target=stat_handler, args=(end_event,), name='stat_handler'), ] + [ @@ -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..117fbdc159 100755 --- a/sunnypilot/sunnylink/uploader.py +++ b/sunnypilot/sunnylink/uploader.py @@ -1,40 +1,38 @@ #!/usr/bin/env python3 -import bz2 -import datetime -import io import json import os import random +import requests import threading import time import traceback +import datetime from collections.abc import Iterator -from typing import BinaryIO -import requests +from cereal import log +import cereal.messaging as messaging +from sunnypilot.sunnylink.api import SunnylinkApi +from openpilot.common.file_helpers import get_upload_stream from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity -from openpilot.common.swaglog import cloudlog from openpilot.system.hardware.hw import Paths from openpilot.system.loggerd.xattr_cache import getxattr, setxattr - -import cereal.messaging as messaging -from cereal import log -from sunnypilot.sunnylink.api import SunnylinkApi +from openpilot.common.swaglog import cloudlog NetworkType = log.DeviceState.NetworkType UPLOAD_ATTR_NAME = 'user.sunny.upload' - UPLOAD_ATTR_VALUE = b'1' -UPLOAD_QLOG_QCAM_MAX_SIZE = 5 * 1e6 # MB +MAX_UPLOAD_SIZES = { + "qlog": 25*1e6, # can't be too restrictive here since we use qlogs to find + # bugs, including ones that can cause massive log sizes + "qcam": 5*1e6, +} allow_sleep = bool(os.getenv("UPLOADER_SLEEP", "1")) force_wifi = os.getenv("FORCEWIFI") is not None fake_upload = os.getenv("FAKEUPLOAD") is not None -OFFROAD_TRANSITION_TIMEOUT = 900. # wait until offroad for 15 minutes before allowing uploads - class FakeRequest: def __init__(self): @@ -52,7 +50,6 @@ def get_directory_sort(d: str) -> list[str]: o = ["0", ] if d.startswith("2024-") else ["1", ] return o + [s.rjust(10, '0') for s in d.rsplit('--', 1)] - def listdir_by_creation(d: str) -> list[str]: if not os.path.isdir(d): return [] @@ -65,7 +62,6 @@ def listdir_by_creation(d: str) -> list[str]: cloudlog.exception("listdir_by_creation failed") return [] - def clear_locks(root: str) -> None: for logdir in os.listdir(root): path = os.path.join(root, logdir) @@ -89,11 +85,11 @@ class Uploader: self.last_filename = "" self.immediate_folders = ["crash/", "boot/"] - self.immediate_priority = {"qlog": 0, "qlog.bz2": 0, "qcamera.ts": 1} + 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") - requested_routes = [] if r is None else r.split(",") + 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): path = os.path.join(self.root, logdir) @@ -137,11 +133,11 @@ class Uploader: if any(f in fn for f in self.immediate_folders): return name, key, fn - return next( - ((name, key, fn) - for name, key, fn in upload_files if name in self.immediate_priority), - None, - ) + for name, key, fn in upload_files: + if name in self.immediate_priority: + return name, key, fn + + return None def do_upload(self, key: str, fn: str): url_resp = self.api.get( @@ -161,15 +157,15 @@ class Uploader: if fake_upload: return FakeResponse() - with open(fn, "rb") as f: - data: BinaryIO - if key.endswith('.bz2') and not fn.endswith('.bz2'): - compressed = bz2.compress(f.read()) - data = io.BytesIO(compressed) - else: - data = f - - return requests.put(url, data=data, headers=headers, timeout=10) + stream = None + try: + compress = key.endswith('.zst') and not fn.endswith('.zst') + stream, _ = get_upload_stream(fn, compress) + response = requests.put(url, data=stream, headers=headers, timeout=10) + return response + finally: + if stream: + stream.close() def upload(self, name: str, key: str, fn: str, network_type: int, metered: bool) -> bool: try: @@ -183,7 +179,7 @@ class Uploader: if sz == 0: # tag files of 0 size as uploaded success = True - elif name in self.immediate_priority and sz > UPLOAD_QLOG_QCAM_MAX_SIZE: + elif name in MAX_UPLOAD_SIZES and sz > MAX_UPLOAD_SIZES[name]: cloudlog.event("uploader_too_large", key=key, fn=fn, sz=sz) success = True else: @@ -224,6 +220,7 @@ class Uploader: return success + def step(self, network_type: int, metered: bool) -> bool | None: d = self.next_file_to_upload(metered) if d is None: @@ -232,8 +229,8 @@ class Uploader: name, key, fn = d # qlogs and bootlogs need to be compressed before uploading - if key.endswith(('qlog', 'rlog')) or (key.startswith('boot/') and not key.endswith('.bz2')): - key += ".bz2" + if key.endswith(('qlog', 'rlog')) or (key.startswith('boot/') and not key.endswith('.zst')): + key += ".zst" return self.upload(name, key, fn, network_type, metered) @@ -250,10 +247,7 @@ def main(exit_event: threading.Event = None) -> None: clear_locks(Paths.log_root()) params = Params() - dongle_id = params.get("SunnylinkDongleId", encoding='utf8') - - offroad_transition_prev = 0. - offroad_last = False + dongle_id = params.get("SunnylinkDongleId") if dongle_id is None: cloudlog.info("uploader missing dongle_id") @@ -265,35 +259,13 @@ def main(exit_event: threading.Event = None) -> None: backoff = 0.1 while not exit_event.is_set(): sm.update(0) - offroad = params.get_bool("IsOffroad") - t = time.monotonic() - if offroad and not offroad_last and t > 300.: - offroad_transition_prev = time.monotonic() - offroad_last = offroad - - network_type = NetworkType.wifi if force_wifi else sm['deviceState'].networkType + network_type = sm['deviceState'].networkType if not force_wifi else NetworkType.wifi if network_type == NetworkType.none: if allow_sleep: time.sleep(60 if offroad else 5) continue - if params.get_bool("DisableOnroadUploads"): - if not offroad or (offroad_transition_prev > 0. and t - offroad_transition_prev < OFFROAD_TRANSITION_TIMEOUT): - if not offroad: - cloudlog.info("not uploading: onroad uploads disabled") - else: - wait_minutes = int(OFFROAD_TRANSITION_TIMEOUT / 60) - time_left = OFFROAD_TRANSITION_TIMEOUT - (t - offroad_transition_prev) - if time_left > 2.0 * 60.0: - time_left_str = f"{int(time_left / 60)} minute(s)" - else: - time_left_str = f"{int(time_left)} seconds(s)" - cloudlog.info(f"not uploading: waiting until offroad for {wait_minutes} minutes; {time_left_str} left") - if allow_sleep: - time.sleep(60) - continue - success = uploader.step(sm['deviceState'].networkType.raw, sm['deviceState'].networkMetered) if success is None: backoff = 60 if offroad else 5 @@ -301,7 +273,7 @@ def main(exit_event: threading.Event = None) -> None: backoff = 0.1 else: cloudlog.info("upload backoff %r", backoff) - backoff = min(backoff * 2, 120) + backoff = min(backoff*2, 120) if allow_sleep: time.sleep(backoff + random.uniform(0, backoff)) 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 018fa0a0b5..f97b8e55bb 100755 --- a/system/athena/athenad.py +++ b/system/athena/athenad.py @@ -23,6 +23,7 @@ from typing import cast from collections.abc import Callable import requests +from requests.adapters import HTTPAdapter, DEFAULT_POOLBLOCK from jsonrpc import JSONRPCResponseManager, dispatcher from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException, create_connection) @@ -56,6 +57,11 @@ WS_FRAME_SIZE = 4096 DEVICE_STATE_UPDATE_INTERVAL = 1.0 # in seconds DEFAULT_UPLOAD_PRIORITY = 99 # higher number = lower priority +# https://bytesolutions.com/dscp-tos-cos-precedence-conversion-chart, +# https://en.wikipedia.org/wiki/Differentiated_services +UPLOAD_TOS = 0x20 # CS1, low priority background traffic +SSH_TOS = 0x90 # AF42, DSCP of 36/HDD_LINUX_AC_VI with the minimum delay flag + NetworkType = log.DeviceState.NetworkType UploadFileDict = dict[str, str | int | float | bool] @@ -64,6 +70,17 @@ UploadItemDict = dict[str, str | bool | int | float | dict[str, str]] UploadFilesToUrlResponse = dict[str, int | list[UploadItemDict] | list[str]] +class UploadTOSAdapter(HTTPAdapter): + def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs): + pool_kwargs["socket_options"] = [(socket.IPPROTO_IP, socket.IP_TOS, UPLOAD_TOS)] + super().init_poolmanager(connections, maxsize, block, **pool_kwargs) + + +UPLOAD_SESS = requests.Session() +UPLOAD_SESS.mount("http://", UploadTOSAdapter()) +UPLOAD_SESS.mount("https://", UploadTOSAdapter()) + + @dataclass class UploadFile: fn: str @@ -136,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") @@ -146,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") @@ -311,10 +328,10 @@ def _do_upload(upload_item: UploadItem, callback: Callable = None) -> requests.R stream = None try: stream, content_length = get_upload_stream(path, compress) - response = requests.put(upload_item.url, - data=CallbackReader(stream, callback, content_length) if callback else stream, - headers={**upload_item.headers, 'Content-Length': str(content_length)}, - timeout=30) + response = UPLOAD_SESS.put(upload_item.url, + data=CallbackReader(stream, callback, content_length) if callback else stream, + headers={**upload_item.headers, 'Content-Length': str(content_length)}, + timeout=30) return response finally: if stream: @@ -424,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, @@ -468,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) @@ -481,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) @@ -501,8 +518,7 @@ def start_local_proxy_shim(global_end_event: threading.Event, local_port: int, w raise Exception("Requested local port not whitelisted") # Set TOS to keep connection responsive while under load. - # DSCP of 36/HDD_LINUX_AC_VI with the minimum delay flag - ws.sock.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, 0x90) + ws.sock.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, SSH_TOS) ssock, csock = socket.socketpair() local_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -535,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(): @@ -583,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) @@ -681,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)) @@ -804,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: @@ -869,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 dd82325815..99ac3b1c6b 100644 --- a/system/athena/tests/test_athenad.py +++ b/system/athena/tests/test_athenad.py @@ -19,7 +19,7 @@ from cereal import messaging from openpilot.common.params import Params from openpilot.common.timeout import Timeout from openpilot.system.athena import athenad -from openpilot.system.athena.athenad import MAX_RETRY_COUNT, dispatcher +from openpilot.system.athena.athenad import MAX_RETRY_COUNT, UPLOAD_SESS, dispatcher from openpilot.system.athena.tests.helpers import HTTPRequestHandler, MockWebsocket, MockApi, EchoSocket from openpilot.selfdrive.test.helpers import http_server_context from openpilot.system.hardware.hw import Paths @@ -29,7 +29,7 @@ def seed_athena_server(host, port): with Timeout(2, 'HTTP Server seeding failed'): while True: try: - requests.put(f'http://{host}:{port}/qlog.zst', data='', timeout=10) + UPLOAD_SESS.put(f'http://{host}:{port}/qlog.zst', data='', timeout=10) break except requests.exceptions.ConnectionError: time.sleep(0.1) @@ -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() @@ -239,10 +239,10 @@ class TestAthenadMethods: @pytest.mark.parametrize("status,retry", [(500,True), (412,False)]) @with_upload_handler def test_upload_handler_retry(self, mocker, host, status, retry): - mock_put = mocker.patch('requests.put') + 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..8cde335929 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 @@ -38,7 +37,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 +140,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 +170,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 +187,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 +200,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 +269,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 +305,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 @@ -335,16 +337,6 @@ def hardware_thread(end_event, hw_queue) -> None: 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 +403,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 +421,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 +441,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/hw.h b/system/hardware/hw.h index d2083a5985..bc9d17dd81 100644 --- a/system/hardware/hw.h +++ b/system/hardware/hw.h @@ -55,4 +55,8 @@ namespace Path { return "/dev/shm"; #endif } + + inline std::string model_root() { + return Hardware::PC() ? Path::comma_home() + "/media/0/models" : "/data/media/0/models"; + } } // namespace Path 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 8e4ba722c4..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) @@ -415,8 +405,8 @@ class Tici(HardwareBase): # *** GPU config *** # https://github.com/commaai/agnos-kernel-sdm845/blob/master/arch/arm64/boot/dts/qcom/sdm845-gpu.dtsi#L216 - sudo_write("0", "/sys/class/kgsl/kgsl-3d0/min_pwrlevel") - sudo_write("0", "/sys/class/kgsl/kgsl-3d0/max_pwrlevel") + sudo_write("1", "/sys/class/kgsl/kgsl-3d0/min_pwrlevel") + sudo_write("1", "/sys/class/kgsl/kgsl-3d0/max_pwrlevel") sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_bus_on") sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_clk_on") sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_rail_on") 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 2d2d4640ed..21de1ff33f 100644 --- a/system/loggerd/loggerd.cc +++ b/system/loggerd/loggerd.cc @@ -62,6 +62,7 @@ struct RemoteEncoder { bool recording = false; bool marked_ready_to_rotate = false; bool seen_first_packet = false; + bool audio_initialized = false; }; size_t write_encode_data(LoggerdState *s, cereal::Event::Reader event, RemoteEncoder &re, const EncoderInfo &encoder_info) { @@ -78,12 +79,7 @@ size_t write_encode_data(LoggerdState *s, cereal::Event::Reader event, RemoteEnc LOGW("%s: dropped %d non iframe packets before init", encoder_info.publish_name, re.dropped_frames); re.dropped_frames = 0; } - // if we aren't actually recording, don't create the writer if (encoder_info.record) { - assert(encoder_info.filename != NULL); - re.writer.reset(new VideoWriter(s->logger.segmentPath().c_str(), - encoder_info.filename, idx.getType() != cereal::EncodeIndex::Type::FULL_H_E_V_C, - edata.getWidth(), edata.getHeight(), encoder_info.fps, idx.getType())); // write the header auto header = edata.getHeader(); re.writer->write((uint8_t *)header.begin(), header.size(), idx.getTimestampEof() / 1000, true, false); @@ -138,12 +134,19 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct // if this is a new segment, we close any possible old segments, move to the new, and process any queued packets if (re.current_segment != s->logger.segment()) { - if (re.recording) { - re.writer.reset(); + // if we aren't actually recording, don't create the writer + if (encoder_info.record) { + assert(encoder_info.filename != NULL); + re.writer.reset(new VideoWriter(s->logger.segmentPath().c_str(), + encoder_info.filename, idx.getType() != cereal::EncodeIndex::Type::FULL_H_E_V_C, + edata.getWidth(), edata.getHeight(), encoder_info.fps, idx.getType())); re.recording = false; + re.audio_initialized = false; } re.current_segment = s->logger.segment(); re.marked_ready_to_rotate = false; + } + if (re.audio_initialized || !encoder_info.include_audio) { // we are in this segment now, process any queued messages before this one if (!re.q.empty()) { for (auto qmsg : re.q) { @@ -153,9 +156,14 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct } re.q.clear(); } + bytes_count += write_encode_data(s, event, re, encoder_info); + delete msg; + } else if (re.q.size() > MAIN_FPS*10) { + LOGE_100("%s: dropping frame waiting for audio initialization, queue is too large", name.c_str()); + delete msg; + } else { + re.q.push_back(msg); // queue up all the new segment messages, they go in after audio is initialized } - bytes_count += write_encode_data(s, event, re, encoder_info); - delete msg; } else if (offset_segment_num > s->logger.segment()) { // encoderd packet has a newer segment, this means encoderd has rolled over if (!re.marked_ready_to_rotate) { @@ -186,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; @@ -214,7 +222,7 @@ void loggerd_thread() { typedef struct ServiceState { std::string name; int counter, freq; - bool encoder, user_flag; + bool encoder, preserve_segment, record_audio; } ServiceState; std::unordered_map service_state; std::unordered_map remote_encoders; @@ -226,19 +234,22 @@ void loggerd_thread() { for (const auto& [_, it] : services) { const bool encoder = util::ends_with(it.name, "EncodeData"); const bool livestream_encoder = util::starts_with(it.name, "livestream"); - if (!it.should_log && (!encoder || livestream_encoder)) continue; - LOGD("logging %s", it.name.c_str()); + const bool record_audio = (it.name == "rawAudioData") && Params().getBool("RecordAudio"); + if (it.should_log || (encoder && !livestream_encoder) || record_audio) { + LOGD("logging %s", it.name.c_str()); - SubSocket * sock = SubSocket::create(ctx.get(), it.name); - assert(sock != NULL); - poller->registerSocket(sock); - service_state[sock] = { - .name = it.name, - .counter = 0, - .freq = it.decimation, - .encoder = encoder, - .user_flag = it.name == "userFlag", - }; + SubSocket * sock = SubSocket::create(ctx.get(), it.name); + assert(sock != NULL); + poller->registerSocket(sock); + service_state[sock] = { + .name = it.name, + .counter = 0, + .freq = it.decimation, + .encoder = encoder, + .preserve_segment = (it.name == "userBookmark") || (it.name == "audioFeedback"), + .record_audio = record_audio, + }; + } } LoggerdState s; @@ -247,6 +258,7 @@ void loggerd_thread() { Params().put("CurrentRoute", s.logger.routeName()); std::map encoder_infos_dict; + std::vector encoders_with_audio; for (const auto &cam : cameras_logged) { for (const auto &encoder_info : cam.encoder_infos) { encoder_infos_dict[encoder_info.publish_name] = encoder_info; @@ -254,6 +266,13 @@ void loggerd_thread() { } } + for (auto &[sock, service] : service_state) { + auto it = encoder_infos_dict.find(service.name); + if (it != encoder_infos_dict.end() && it->second.include_audio) { + encoders_with_audio.push_back(&remote_encoders[sock]); + } + } + uint64_t msg_count = 0, bytes_count = 0; double start_ts = millis_since_boot(); while (!do_exit) { @@ -262,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 @@ -271,6 +290,20 @@ void loggerd_thread() { Message *msg = nullptr; while (!do_exit && (msg = sock->receive(true))) { const bool in_qlog = service.freq != -1 && (service.counter++ % service.freq == 0); + + if (service.record_audio) { + capnp::FlatArrayMessageReader cmsg(kj::ArrayPtr((capnp::word *)msg->getData(), msg->getSize() / sizeof(capnp::word))); + auto event = cmsg.getRoot(); + auto audio_data = event.getRawAudioData().getData(); + auto sample_rate = event.getRawAudioData().getSampleRate(); + for (auto* encoder : encoders_with_audio) { + if (encoder && encoder->writer) { + encoder->writer->write_audio((uint8_t*)audio_data.begin(), audio_data.size(), event.getLogMonoTime() / 1000, sample_rate); + encoder->audio_initialized = true; + } + } + } + if (service.encoder) { s.last_camera_seen_tms = millis_since_boot(); bytes_count += handle_encoder_msg(&s, msg, service.name, remote_encoders[sock], encoder_infos_dict[service.name]); diff --git a/system/loggerd/loggerd.h b/system/loggerd/loggerd.h index 27d2d37fc4..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,18 +26,42 @@ 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; const char *thumbnail_name = NULL; const char *filename = NULL; bool record = true; + bool include_audio = false; 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)(); @@ -58,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), }; @@ -71,41 +94,39 @@ 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"), INIT_ENCODE_FUNCTIONS(QRoadEncode), }; 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 179e237a65..c6a4b12e63 100644 --- a/system/loggerd/tests/test_loggerd.py +++ b/system/loggerd/tests/test_loggerd.py @@ -97,6 +97,50 @@ class TestLoggerd: return sent_msgs + def _publish_camera_and_audio_messages(self, num_segs=1, segment_length=5): + d = DEVICE_CAMERAS[("tici", "ar0231")] + streams = [ + (VisionStreamType.VISION_STREAM_ROAD, (d.fcam.width, d.fcam.height, 2048 * 2346, 2048, 2048 * 1216), "roadCameraState"), + (VisionStreamType.VISION_STREAM_DRIVER, (d.dcam.width, d.dcam.height, 2048 * 2346, 2048, 2048 * 1216), "driverCameraState"), + (VisionStreamType.VISION_STREAM_WIDE_ROAD, (d.ecam.width, d.ecam.height, 2048 * 2346, 2048, 2048 * 1216), "wideRoadCameraState"), + ] + + pm = messaging.PubMaster([s for _, _, s in streams] + ["rawAudioData"]) + vipc_server = VisionIpcServer("camerad") + for stream_type, frame_spec, _ in streams: + vipc_server.create_buffers_with_sizes(stream_type, 40, *(frame_spec)) + vipc_server.start_listener() + + os.environ["LOGGERD_TEST"] = "1" + os.environ["LOGGERD_SEGMENT_LENGTH"] = str(segment_length) + managed_processes["loggerd"].start() + managed_processes["encoderd"].start() + assert pm.wait_for_readers_to_update("roadCameraState", timeout=5) + + fps = 20 + for n in range(1, int(num_segs * segment_length * fps) + 1): + # send video + for stream_type, frame_spec, state in streams: + dat = np.empty(frame_spec[2], dtype=np.uint8) + vipc_server.send(stream_type, dat[:].flatten().tobytes(), n, n / fps, n / fps) + + camera_state = messaging.new_message(state) + frame = getattr(camera_state, state) + frame.frameId = n + pm.send(state, camera_state) + + # send audio + msg = messaging.new_message('rawAudioData') + msg.rawAudioData.data = bytes(800 * 2) # 800 samples of int16 + msg.rawAudioData.sampleRate = 16000 + pm.send('rawAudioData', msg) + + for _, _, state in streams: + assert pm.wait_for_readers_to_update(state, timeout=5, dt=0.001) + + managed_processes["loggerd"].stop() + managed_processes["encoderd"].stop() + def test_init_data_values(self): os.environ["CLEAN"] = random.choice(["0", "1"]) @@ -136,53 +180,23 @@ class TestLoggerd: assert getattr(initData, initData_key) == v assert logged_params[param_key].decode() == v - @pytest.mark.skip("FIXME: encoderd sometimes crashes in CI when running with pytest-xdist") + @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): - os.environ["LOGGERD_TEST"] = "1" - Params().put("RecordFront", "1") + Params().put("RecordFront", True) - d = DEVICE_CAMERAS[("tici", "ar0231")] expected_files = {"rlog.zst", "qlog.zst", "qcamera.ts", "fcamera.hevc", "dcamera.hevc", "ecamera.hevc"} - streams = [(VisionStreamType.VISION_STREAM_ROAD, (d.fcam.width, d.fcam.height, 2048*2346, 2048, 2048*1216), "roadCameraState"), - (VisionStreamType.VISION_STREAM_DRIVER, (d.dcam.width, d.dcam.height, 2048*2346, 2048, 2048*1216), "driverCameraState"), - (VisionStreamType.VISION_STREAM_WIDE_ROAD, (d.ecam.width, d.ecam.height, 2048*2346, 2048, 2048*1216), "wideRoadCameraState")] - pm = messaging.PubMaster(["roadCameraState", "driverCameraState", "wideRoadCameraState"]) - vipc_server = VisionIpcServer("camerad") - for stream_type, frame_spec, _ in streams: - vipc_server.create_buffers_with_sizes(stream_type, 40, *(frame_spec)) - vipc_server.start_listener() + num_segs = random.randint(2, 3) + length = random.randint(4, 5) # H264 encoder uses 40 lookahead frames and does B-frame reordering, so minimum 3 seconds before qcam output - num_segs = random.randint(2, 5) - length = random.randint(1, 3) - os.environ["LOGGERD_SEGMENT_LENGTH"] = str(length) - managed_processes["loggerd"].start() - managed_processes["encoderd"].start() - assert pm.wait_for_readers_to_update("roadCameraState", timeout=5) - - fps = 20.0 - for n in range(1, int(num_segs*length*fps)+1): - for stream_type, frame_spec, state in streams: - dat = np.empty(frame_spec[2], dtype=np.uint8) - vipc_server.send(stream_type, dat[:].flatten().tobytes(), n, n/fps, n/fps) - - camera_state = messaging.new_message(state) - frame = getattr(camera_state, state) - frame.frameId = n - pm.send(state, camera_state) - - for _, _, state in streams: - assert pm.wait_for_readers_to_update(state, timeout=5, dt=0.001) - - managed_processes["loggerd"].stop() - managed_processes["encoderd"].stop() + self._publish_camera_and_audio_messages(num_segs=num_segs, segment_length=length) route_path = str(self._get_latest_log_dir()).rsplit("--", 1)[0] for n in range(num_segs): p = Path(f"{route_path}--{n}") logged = {f.name for f in p.iterdir() if f.is_file()} diff = logged ^ expected_files - assert len(diff) == 0, f"didn't get all expected files. run={_} seg={n} {route_path=}, {diff=}\n{logged=} {expected_files=}" + assert len(diff) == 0, f"didn't get all expected files. seg={n} {route_path=}, {diff=}\n{logged=} {expected_files=}" def test_bootlog(self): # generate bootlog with fake launch log @@ -268,16 +282,43 @@ 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() assert getxattr(segment_dir, PRESERVE_ATTR_NAME) is None + + @pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing + @pytest.mark.parametrize("record_front", [True, False]) + def test_record_front(self, record_front): + params = Params() + params.put_bool("RecordFront", record_front) + + self._publish_camera_and_audio_messages() + + dcamera_hevc_exists = os.path.exists(os.path.join(self._get_latest_log_dir(), 'dcamera.hevc')) + assert dcamera_hevc_exists == record_front + + @pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing + @pytest.mark.parametrize("record_audio", [True, False]) + def test_record_audio(self, record_audio): + params = Params() + params.put_bool("RecordAudio", record_audio) + + self._publish_camera_and_audio_messages() + + qcamera_ts_path = os.path.join(self._get_latest_log_dir(), 'qcamera.ts') + ffprobe_cmd = f"ffprobe -i {qcamera_ts_path} -show_streams -select_streams a -loglevel error" + has_audio_stream = subprocess.run(ffprobe_cmd, shell=True, capture_output=True).stdout.strip() != b'' + assert has_audio_stream == record_audio + + raw_audio_in_rlog = any(m.which() == 'rawAudioData' for m in LogReader(os.path.join(self._get_latest_log_dir(), 'rlog.zst'))) + assert raw_audio_in_rlog == record_audio 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 90b5f1af3d..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" @@ -50,6 +49,45 @@ VideoWriter::VideoWriter(const char *path, const char *filename, bool remuxing, } } +void VideoWriter::initialize_audio(int sample_rate) { + assert(this->ofmt_ctx->oformat->audio_codec != AV_CODEC_ID_NONE); // check output format supports audio streams + const AVCodec *audio_avcodec = avcodec_find_encoder(AV_CODEC_ID_AAC); + assert(audio_avcodec); + this->audio_codec_ctx = avcodec_alloc_context3(audio_avcodec); + assert(this->audio_codec_ctx); + this->audio_codec_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP; + this->audio_codec_ctx->sample_rate = sample_rate; + #if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 28, 100) // FFmpeg 5.1+ + av_channel_layout_default(&this->audio_codec_ctx->ch_layout, 1); + #else + this->audio_codec_ctx->channel_layout = AV_CH_LAYOUT_MONO; + #endif + this->audio_codec_ctx->bit_rate = 32000; + this->audio_codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + this->audio_codec_ctx->time_base = (AVRational){1, audio_codec_ctx->sample_rate}; + int err = avcodec_open2(this->audio_codec_ctx, audio_avcodec, NULL); + assert(err >= 0); + av_log_set_level(AV_LOG_WARNING); // hide "QAvg" info msgs at the end of every segment + + this->audio_stream = avformat_new_stream(this->ofmt_ctx, NULL); + assert(this->audio_stream); + err = avcodec_parameters_from_context(this->audio_stream->codecpar, this->audio_codec_ctx); + assert(err >= 0); + + this->audio_frame = av_frame_alloc(); + assert(this->audio_frame); + this->audio_frame->format = this->audio_codec_ctx->sample_fmt; + #if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 28, 100) // FFmpeg 5.1+ + av_channel_layout_copy(&this->audio_frame->ch_layout, &this->audio_codec_ctx->ch_layout); + #else + this->audio_frame->channel_layout = this->audio_codec_ctx->channel_layout; + #endif + this->audio_frame->sample_rate = this->audio_codec_ctx->sample_rate; + this->audio_frame->nb_samples = this->audio_codec_ctx->frame_size; + err = av_frame_get_buffer(this->audio_frame, 0); + assert(err >= 0); +} + void VideoWriter::write(uint8_t *data, int len, long long timestamp, bool codecconfig, bool keyframe) { if (of && data) { size_t written = util::safe_fwrite(data, 1, len, of); @@ -67,16 +105,18 @@ void VideoWriter::write(uint8_t *data, int len, long long timestamp, bool codecc } int err = avcodec_parameters_from_context(out_stream->codecpar, codec_ctx); assert(err >= 0); + // if there is an audio stream, it must be initialized before this point err = avformat_write_header(ofmt_ctx, NULL); assert(err >= 0); + header_written = true; } else { // 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; enum AVRounding rnd = static_cast(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX); pkt.pts = pkt.dts = av_rescale_q_rnd(timestamp, in_timebase, ofmt_ctx->streams[0]->time_base, rnd); @@ -95,11 +135,93 @@ void VideoWriter::write(uint8_t *data, int len, long long timestamp, bool codecc } } +void VideoWriter::write_audio(uint8_t *data, int len, long long timestamp, int sample_rate) { + if (!remuxing) return; + if (!audio_initialized) { + initialize_audio(sample_rate); + audio_initialized = true; + } + if (!audio_codec_ctx) return; + // sync logMonoTime of first audio packet with the timestampEof of first video packet + if (audio_pts == 0) { + audio_pts = (timestamp * audio_codec_ctx->sample_rate) / 1000000ULL; + } + + // convert s16le samples to fltp and add to buffer + const int16_t *raw_samples = reinterpret_cast(data); + int sample_count = len / sizeof(int16_t); + constexpr float normalizer = 1.0f / 32768.0f; + + const size_t max_buffer_size = sample_rate * 10; // 10 seconds + if (audio_buffer.size() + sample_count > max_buffer_size) { + size_t samples_to_drop = (audio_buffer.size() + sample_count) - max_buffer_size; + LOGE("Audio buffer overflow, dropping %zu oldest samples", samples_to_drop); + audio_buffer.erase(audio_buffer.begin(), audio_buffer.begin() + samples_to_drop); + audio_pts += samples_to_drop; + } + + // Add new samples to the buffer + const size_t original_size = audio_buffer.size(); + audio_buffer.resize(original_size + sample_count); + std::transform(raw_samples, raw_samples + sample_count, audio_buffer.begin() + original_size, + [](int16_t sample) { return sample * normalizer; }); + + if (!header_written) return; // header not written yet, process audio frame after header is written + while (audio_buffer.size() >= audio_codec_ctx->frame_size) { + audio_frame->pts = audio_pts; + float *f_samples = reinterpret_cast(audio_frame->data[0]); + std::copy(audio_buffer.begin(), audio_buffer.begin() + audio_codec_ctx->frame_size, f_samples); + audio_buffer.erase(audio_buffer.begin(), audio_buffer.begin() + audio_codec_ctx->frame_size); + encode_and_write_audio_frame(audio_frame); + } +} + +void VideoWriter::encode_and_write_audio_frame(AVFrame* frame) { + if (!remuxing || !audio_codec_ctx) return; + int send_result = avcodec_send_frame(audio_codec_ctx, frame); // encode frame + if (send_result >= 0) { + AVPacket *pkt = av_packet_alloc(); + while (avcodec_receive_packet(audio_codec_ctx, pkt) == 0) { + av_packet_rescale_ts(pkt, audio_codec_ctx->time_base, audio_stream->time_base); + pkt->stream_index = audio_stream->index; + + int err = av_interleaved_write_frame(ofmt_ctx, pkt); // write encoded frame + if (err < 0) { + LOGW("AUDIO: Write frame failed - error: %d", err); + } + av_packet_unref(pkt); + } + av_packet_free(&pkt); + } else { + LOGW("AUDIO: Failed to send audio frame to encoder: %d", send_result); + } + 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); + } int err = av_write_trailer(this->ofmt_ctx); if (err != 0) LOGE("av_write_trailer failed %d", err); avcodec_free_context(&this->codec_ctx); + if (this->audio_frame) av_frame_free(&this->audio_frame); err = avio_closep(&this->ofmt_ctx->pb); if (err != 0) LOGE("avio_closep failed %d", err); avformat_free_context(this->ofmt_ctx); diff --git a/system/loggerd/video_writer.h b/system/loggerd/video_writer.h index 1aa758b42b..e973c5d811 100644 --- a/system/loggerd/video_writer.h +++ b/system/loggerd/video_writer.h @@ -1,6 +1,7 @@ #pragma once #include +#include extern "C" { #include @@ -13,13 +14,29 @@ class VideoWriter { public: VideoWriter(const char *path, const char *filename, bool remuxing, int width, int height, int fps, cereal::EncodeIndex::Type codec); void write(uint8_t *data, int len, long long timestamp, bool codecconfig, bool keyframe); + void write_audio(uint8_t *data, int len, long long timestamp, int sample_rate); + ~VideoWriter(); + 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; AVCodecContext *codec_ctx; AVFormatContext *ofmt_ctx; AVStream *out_stream; + + bool audio_initialized = false; + bool header_written = false; + AVStream *audio_stream = nullptr; + AVCodecContext *audio_codec_ctx = nullptr; + AVFrame *audio_frame = nullptr; + uint64_t audio_pts = 0; + std::deque audio_buffer; + bool remuxing; }; diff --git a/system/manager/manager.py b/system/manager/manager.py index 3910ac5526..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,60 +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"), - ("CustomAccIncrementsEnabled", "0"), - ("CustomAccLongPressIncrement", "5"), - ("CustomAccShortPressIncrement", "1"), - ("DeviceBootMode", "0"), - ("DynamicExperimentalControl", "0"), - ("HyundaiLongitudinalTuning", "0"), - ("LagdToggle", "1"), - ("HyundaiRadar", "0"), - ("Mads", "1"), - ("MadsMainCruiseAllowed", "1"), - ("MadsSteeringMode", "0"), - ("MadsUnifiedEngagementMode", "1"), - ("MapdVersion", f"{VERSION}"), - ("MaxTimeOffroad", "1800"), - ("Brightness", "0"), - ("ModelManager_LastSyncTime", "0"), - ("ModelManager_ModelsCache", ""), - ("NeuralNetworkLateralControl", "0"), - ("QuietMode", "0"), - ] + 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: @@ -100,6 +64,7 @@ def manager_init() -> None: params.put("GitCommitDate", build_metadata.openpilot.git_commit_date) params.put("GitBranch", build_metadata.channel) params.put("GitRemote", build_metadata.openpilot.git_origin) + params.put_bool("IsDevelopmentBranch", build_metadata.development_channel) params.put_bool("IsTestedBranch", build_metadata.tested_channel) params.put_bool("IsReleaseBranch", build_metadata.release_channel) params.put("HardwareSerial", serial) @@ -153,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) @@ -173,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) @@ -195,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 2e3f754a76..a9579546ba 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -88,6 +88,9 @@ def is_stock_model(started, params, CP: car.CarParams) -> bool: """Check if the active model runner is stock.""" return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.stock) +def mapd_ready(started: bool, params: Params, CP: car.CarParams) -> bool: + return bool(os.path.exists(Paths.mapd_root())) + def or_(*fns): return lambda *args: operator.or_(*(fn(*args) for fn in fns)) @@ -114,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), @@ -139,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), @@ -162,14 +167,14 @@ procs += [ PythonProcess("backup_manager", "sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)), # mapd - NativeProcess("mapd", Paths.mapd_root(), [MAPD_PATH], always_run), + NativeProcess("mapd", Paths.mapd_root(), ["bash", "-c", f"{MAPD_PATH} > /dev/null 2>&1"], mapd_ready), PythonProcess("mapd_manager", "sunnypilot.mapd.mapd_manager", always_run), ] if os.path.exists("./github_runner.sh"): procs += [NativeProcess("github_runner_start", "system/manager", ["./github_runner.sh", "start"], and_(only_offroad, use_github_runner), sigkill=False)] -if os.path.exists("../sunnypilot/sunnylink/uploader.py"): +if os.path.exists("../../sunnypilot/sunnylink/uploader.py"): procs += [PythonProcess("sunnylink_uploader", "sunnypilot.sunnylink.uploader", use_sunnylink_uploader_shim)] managed_processes = {p.name: p for p in procs} 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/micd.py b/system/micd.py index 38f3225f55..02ef82390b 100755 --- a/system/micd.py +++ b/system/micd.py @@ -9,10 +9,10 @@ from openpilot.common.retry import retry from openpilot.common.swaglog import cloudlog RATE = 10 -FFT_SAMPLES = 4096 +FFT_SAMPLES = 1600 # 100ms REFERENCE_SPL = 2e-5 # newtons/m^2 -SAMPLE_RATE = 44100 -SAMPLE_BUFFER = 4096 # approx 100ms +SAMPLE_RATE = 16000 +SAMPLE_BUFFER = 800 # 50ms @cache @@ -45,7 +45,7 @@ def apply_a_weighting(measurements: np.ndarray) -> np.ndarray: class Mic: def __init__(self): self.rk = Ratekeeper(RATE) - self.pm = messaging.PubMaster(['microphone']) + self.pm = messaging.PubMaster(['soundPressure', 'rawAudioData']) self.measurements = np.empty(0) @@ -61,12 +61,12 @@ class Mic: sound_pressure_weighted = self.sound_pressure_weighted sound_pressure_level_weighted = self.sound_pressure_level_weighted - msg = messaging.new_message('microphone', valid=True) - msg.microphone.soundPressure = float(sound_pressure) - msg.microphone.soundPressureWeighted = float(sound_pressure_weighted) - msg.microphone.soundPressureWeightedDb = float(sound_pressure_level_weighted) + msg = messaging.new_message('soundPressure', valid=True) + msg.soundPressure.soundPressure = float(sound_pressure) + msg.soundPressure.soundPressureWeighted = float(sound_pressure_weighted) + msg.soundPressure.soundPressureWeightedDb = float(sound_pressure_level_weighted) - self.pm.send('microphone', msg) + self.pm.send('soundPressure', msg) self.rk.keep_time() def callback(self, indata, frames, time, status): @@ -76,6 +76,12 @@ class Mic: Logged A-weighted equivalents are rough approximations of the human-perceived loudness. """ + msg = messaging.new_message('rawAudioData', valid=True) + audio_data_int_16 = (indata[:, 0] * 32767).astype(np.int16) + msg.rawAudioData.data = audio_data_int_16.tobytes() + msg.rawAudioData.sampleRate = SAMPLE_RATE + self.pm.send('rawAudioData', msg) + with self.lock: self.measurements = np.concatenate((self.measurements, indata[:, 0])) 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/sensord/sensors/lsm6ds3_temp.py b/system/sensord/sensors/lsm6ds3_temp.py index 7d7b1c2ce1..b9bb9fe3da 100644 --- a/system/sensord/sensors/lsm6ds3_temp.py +++ b/system/sensord/sensors/lsm6ds3_temp.py @@ -7,10 +7,10 @@ from openpilot.system.sensord.sensors.i2c_sensor import Sensor class LSM6DS3_Temp(Sensor): @property def device_address(self) -> int: - return 0x6A # Default I2C address for LSM6DS3 + return 0x6A def _read_temperature(self) -> float: - scale = 16.0 if log.SensorEventData.SensorSource.lsm6ds3 else 256.0 + scale = 16.0 if self.source == log.SensorEventData.SensorSource.lsm6ds3 else 256.0 data = self.read(0x20, 2) return 25 + (self.parse_16bit(data[0], data[1]) / scale) 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 2194a4b9d2..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({ @@ -136,6 +136,17 @@ class TTYPigeon: return True return False +def save_almanac(pigeon: TTYPigeon) -> None: + # store almanac in flash + pigeon.send(b"\xB5\x62\x09\x14\x04\x00\x00\x00\x00\x00\x21\xEC") + try: + if pigeon.wait_for_ack(ack=UBLOX_SOS_ACK, nack=UBLOX_SOS_NACK): + cloudlog.info("Done storing almanac") + else: + cloudlog.error("Error storing almanac") + except TimeoutError: + pass + def init_baudrate(pigeon: TTYPigeon): # ublox default setting on startup is 9600 baudrate pigeon.set_baud(9600) @@ -146,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: @@ -239,32 +250,17 @@ 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") - # store almanac in flash - pigeon.send(b"\xB5\x62\x09\x14\x04\x00\x00\x00\x00\x00\x21\xEC") - try: - if pigeon.wait_for_ack(ack=UBLOX_SOS_ACK, nack=UBLOX_SOS_NACK): - cloudlog.warning("Done storing almanac") - else: - cloudlog.error("Error storing almanac") - except TimeoutError: - pass - # turn off power and exit cleanly 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) @@ -272,28 +268,34 @@ 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 - - while end_condition(): + last_almanac_save = time.monotonic() + 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 msg = messaging.new_message('ubloxRaw', len(dat), valid=True) msg.ubloxRaw = dat[:] pm.send('ubloxRaw', msg) + + # save almanac every 5 minutes + if (time.monotonic() - last_almanac_save) > 60*5: + save_almanac(pigeon) + last_almanac_save = time.monotonic() else: # prevent locking up a CPU core if ublox disconnects time.sleep(0.001) @@ -301,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 805fdcc71f..718bf036fa 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -3,21 +3,28 @@ import cffi import os import time import pyray as rl +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 -from openpilot.system.hardware import HARDWARE +from openpilot.system.hardware import HARDWARE, PC +from openpilot.common.realtime import Ratekeeper -DEFAULT_FPS = 60 +DEFAULT_FPS = int(os.getenv("FPS", "60")) 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", "1") == "1" -SHOW_FPS = os.getenv("SHOW_FPS") == '1' -STRICT_MODE = os.getenv("STRICT_MODE") == '1' +ENABLE_VSYNC = os.getenv("ENABLE_VSYNC", "0") == "1" +SHOW_FPS = os.getenv("SHOW_FPS") == "1" +SHOW_TOUCHES = os.getenv("SHOW_TOUCHES") == "1" +STRICT_MODE = os.getenv("STRICT_MODE") == "1" SCALE = float(os.getenv("SCALE", "1.0")) DEFAULT_TEXT_SIZE = 60 @@ -27,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 @@ -45,6 +52,71 @@ class ModalOverlay: callback: Callable | None = None +class MousePos(NamedTuple): + x: float + y: float + + +class MouseEvent(NamedTuple): + pos: MousePos + slot: int + left_pressed: bool + left_released: bool + left_down: bool + t: float + + +class MouseState: + def __init__(self): + self._events: deque[MouseEvent] = deque(maxlen=MOUSE_THREAD_RATE) # bound event list + self._prev_mouse_event: list[MouseEvent | None] = [None] * MAX_TOUCH_SLOTS + + self._rk = Ratekeeper(MOUSE_THREAD_RATE) + self._lock = threading.Lock() + self._exit_event = threading.Event() + self._thread = None + + def get_events(self) -> list[MouseEvent]: + with self._lock: + events = list(self._events) + self._events.clear() + return events + + def start(self): + self._exit_event.clear() + if self._thread is None or not self._thread.is_alive(): + self._thread = threading.Thread(target=self._run_thread, daemon=True) + self._thread.start() + + def stop(self): + self._exit_event.set() + if self._thread is not None and self._thread.is_alive(): + self._thread.join() + + def _run_thread(self): + while not self._exit_event.is_set(): + rl.poll_input_events() + self._handle_mouse_event() + self._rk.keep_time() + + def _handle_mouse_event(self): + 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: def __init__(self, width: int, height: int): self._fonts: dict[FontWeight, rl.Font] = {} @@ -61,6 +133,12 @@ class GuiApplication: self._trace_log_callback = None self._modal_overlay = ModalOverlay() + self._mouse = MouseState() + self._mouse_events: list[MouseEvent] = [] + + # Debug variables + self._mouse_history: deque[MousePos] = deque(maxlen=MOUSE_THREAD_RATE) + def request_close(self): self._window_close_requested = True @@ -89,6 +167,9 @@ class GuiApplication: self._set_styles() self._load_fonts() + if not PC: + self._mouse.start() + def set_modal_overlay(self, overlay, callback: Callable | None = None): self._modal_overlay = ModalOverlay(overlay=overlay, callback=callback) @@ -149,11 +230,25 @@ class GuiApplication: rl.unload_render_texture(self._render_texture) self._render_texture = None + if not PC: + self._mouse.stop() + rl.close_window() + @property + def mouse_events(self) -> list[MouseEvent]: + return self._mouse_events + def render(self): try: while not (self._window_close_requested or rl.window_should_close()): + if PC: + # Thread is not used on PC, need to manually add mouse events + self._mouse._handle_mouse_event() + + # Store all mouse events for the current frame + self._mouse_events = self._mouse.get_events() + if self._render_texture: rl.begin_texture_mode(self._render_texture) rl.clear_background(rl.BLACK) @@ -190,6 +285,20 @@ class GuiApplication: if SHOW_FPS: rl.draw_fps(10, 10) + if SHOW_TOUCHES: + for mouse_event in self._mouse_events: + if mouse_event.left_pressed: + self._mouse_history.clear() + self._mouse_history.append(mouse_event.pos) + + if self._mouse_history: + mouse_pos = self._mouse_history[-1] + rl.draw_circle(int(mouse_pos.x), int(mouse_pos.y), 15, rl.RED) + for idx, mouse_pos in enumerate(self._mouse_history): + perc = idx / len(self._mouse_history) + color = rl.Color(min(int(255 * (1.5 - perc)), 255), int(min(255 * (perc + 0.5), 255)), 50, 255) + rl.draw_circle(int(mouse_pos.x), int(mouse_pos.y), 5, color) + rl.end_drawing() self._monitor_fps() except KeyboardInterrupt: @@ -207,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 32f3b7b575..e2296fd5ed 100644 --- a/system/ui/lib/scroll_panel.py +++ b/system/ui/lib/scroll_panel.py @@ -1,6 +1,8 @@ +import time import pyray as rl from collections import deque from enum import IntEnum +from openpilot.system.ui.lib.application import gui_app, MouseEvent, MousePos # Scroll constants for smooth scrolling behavior MOUSE_WHEEL_SCROLL_SPEED = 30 @@ -38,51 +40,55 @@ class GuiScrollPanel: self._bounds_rect: rl.Rectangle | None = None 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), 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): # Store rectangles for reference self._content_rect = content self._bounds_rect = bounds - # Calculate time delta - current_time = rl.get_time() - - mouse_pos = rl.get_mouse_position() max_scroll_y = max(content.height - bounds.height, 0) # Start dragging on mouse press - if rl.check_collision_point_rec(mouse_pos, bounds) and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): + if rl.check_collision_point_rec(mouse_event.pos, bounds) and mouse_event.left_pressed: if self._scroll_state == ScrollState.IDLE or self._scroll_state == ScrollState.BOUNCING: self._scroll_state = ScrollState.DRAGGING_CONTENT if self._show_vertical_scroll_bar: scrollbar_width = rl.gui_get_style(rl.GuiControl.LISTVIEW, rl.GuiListViewProperty.SCROLLBAR_WIDTH) scrollbar_x = bounds.x + bounds.width - scrollbar_width - if mouse_pos.x >= scrollbar_x: + if mouse_event.pos.x >= scrollbar_x: self._scroll_state = ScrollState.DRAGGING_SCROLLBAR # TODO: hacky # when clicking while moving, go straight into dragging self._is_dragging = abs(self._velocity_y) > MIN_VELOCITY - self._last_mouse_y = mouse_pos.y - self._start_mouse_y = mouse_pos.y - self._last_drag_time = current_time + self._last_mouse_y = mouse_event.pos.y + self._start_mouse_y = mouse_event.pos.y + self._last_drag_time = mouse_event.t self._velocity_history.clear() self._velocity_y = 0.0 self._bounce_offset = 0.0 # Handle active dragging if self._scroll_state == ScrollState.DRAGGING_CONTENT or self._scroll_state == ScrollState.DRAGGING_SCROLLBAR: - if rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT): - delta_y = mouse_pos.y - self._last_mouse_y + if mouse_event.left_down: + delta_y = mouse_event.pos.y - self._last_mouse_y # Track velocity for inertia - time_since_last_drag = current_time - self._last_drag_time + time_since_last_drag = mouse_event.t - self._last_drag_time if time_since_last_drag > 0: - drag_velocity = delta_y / time_since_last_drag / 60.0 + # TODO: HACK: /2 since we usually get two touch events per frame + drag_velocity = delta_y / time_since_last_drag / 60.0 / 2 # TODO: shouldn't be hardcoded self._velocity_history.append(drag_velocity) - self._last_drag_time = current_time + self._last_drag_time = mouse_event.t # Detect actual dragging - total_drag = abs(mouse_pos.y - self._start_mouse_y) + total_drag = abs(mouse_event.pos.y - self._start_mouse_y) if total_drag > DRAG_THRESHOLD: self._is_dragging = True @@ -96,9 +102,9 @@ class GuiScrollPanel: scroll_ratio = content.height / bounds.height self._offset.y -= delta_y * scroll_ratio - self._last_mouse_y = mouse_pos.y + self._last_mouse_y = mouse_event.pos.y - elif rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT): + elif mouse_event.left_released: # Calculate flick velocity if self._velocity_history: total_weight = 0 @@ -167,8 +173,6 @@ class GuiScrollPanel: elif self._offset.y < -(max_scroll_y + MAX_BOUNCE_DISTANCE): self._offset.y = -(max_scroll_y + MAX_BOUNCE_DISTANCE) - return self._offset - def is_touch_valid(self): return not self._is_dragging diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index dd08b66373..178bbec43e 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 @@ -101,8 +101,8 @@ class WifiManager: """Connect to the DBus system bus.""" try: self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect() - if not await self._find_wifi_device(): - raise ValueError("No Wi-Fi device found") + while not await self._find_wifi_device(): + await asyncio.sleep(1) await self._setup_signals(self.device_path) self.active_ap_path = await self.get_active_access_point() @@ -204,7 +204,7 @@ class WifiManager: 'connection': { 'type': Variant('s', '802-11-wireless'), 'uuid': Variant('s', str(uuid.uuid4())), - 'id': Variant('s', ssid), + 'id': Variant('s', f'openpilot connection {ssid}'), 'autoconnect-retries': Variant('i', 0), }, '802-11-wireless': { @@ -212,7 +212,10 @@ class WifiManager: 'hidden': Variant('b', is_hidden), 'mode': Variant('s', 'infrastructure'), }, - 'ipv4': {'method': Variant('s', 'auto')}, + 'ipv4': { + 'method': Variant('s', 'auto'), + 'dns-priority': Variant('i', 600), + }, 'ipv6': {'method': Variant('s', 'ignore')}, } @@ -438,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: @@ -483,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: @@ -494,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.""" @@ -531,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 @@ -540,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 52% rename from system/ui/lib/widget.py rename to system/ui/widgets/__init__.py index 3539f5594b..d45f48ac38 100644 --- a/system/ui/lib/widget.py +++ b/system/ui/widgets/__init__.py @@ -2,6 +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, MAX_TOUCH_SLOTS class DialogResult(IntEnum): @@ -14,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) @@ -48,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 @@ -66,18 +82,37 @@ class Widget(abc.ABC): ret = self._render(self._rect) # Keep track of whether mouse down started within the widget's rectangle - mouse_pos = rl.get_mouse_position() - if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._touch_valid(): - if rl.check_collision_point_rec(mouse_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 rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT): - if self._is_pressed and rl.check_collision_point_rec(mouse_pos, self._rect): - self._handle_mouse_release(mouse_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 @@ -91,6 +126,6 @@ class Widget(abc.ABC): def _update_layout_rects(self) -> None: """Optionally update any layout rects on Widget rect change.""" - def _handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool: + def _handle_mouse_release(self, mouse_pos: MousePos) -> bool: """Optionally handle mouse release events.""" return False 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..a5dab19789 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 = { @@ -29,8 +32,11 @@ BUTTON_TEXT_COLOR = { ButtonStyle.PRIMARY: rl.Color(228, 228, 228, 255), ButtonStyle.DANGER: rl.Color(228, 228, 228, 255), ButtonStyle.TRANSPARENT: rl.BLACK, - ButtonStyle.ACTION: rl.Color(0, 0, 0, 255), + ButtonStyle.ACTION: rl.BLACK, 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 f5581be23e..e871eef0a1 100644 --- a/system/ui/lib/list_view.py +++ b/system/ui/widgets/list_view.py @@ -2,12 +2,12 @@ import os import pyray as rl from collections.abc import Callable from abc import ABC -from openpilot.system.ui.lib.application import gui_app, FontWeight +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: @@ -229,7 +229,7 @@ class ListItem(Widget): super().set_parent_rect(parent_rect) self._rect.width = parent_rect.width - def _handle_mouse_release(self, mouse_pos: rl.Vector2): + def _handle_mouse_release(self, mouse_pos: MousePos): if not self.is_visible: return 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 92% rename from system/ui/lib/toggle.py rename to system/ui/widgets/toggle.py index 8ed9a655ec..eba5ef9e58 100644 --- a/system/ui/lib/toggle.py +++ b/system/ui/widgets/toggle.py @@ -1,5 +1,6 @@ import pyray as rl -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.lib.application import MousePos +from openpilot.system.ui.widgets import Widget ON_COLOR = rl.Color(51, 171, 76, 255) OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255) @@ -23,7 +24,7 @@ class Toggle(Widget): def set_rect(self, rect: rl.Rectangle): self._rect = rl.Rectangle(rect.x, rect.y, WIDTH, HEIGHT) - def _handle_mouse_release(self, mouse_pos: rl.Vector2): + def _handle_mouse_release(self, mouse_pos: MousePos): if not self._enabled: return @@ -37,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 11c6ced2b7..9bf8855a28 100755 --- a/system/version.py +++ b/system/version.py @@ -12,14 +12,14 @@ from openpilot.common.git import get_commit, get_origin, get_branch, get_short_b RELEASE_SP_BRANCHES = ['release-c3'] TESTED_SP_BRANCHES = ['staging-c3', 'staging-c3-new'] -MASTER_SP_BRANCHES = ['master', 'master-new'] +MASTER_SP_BRANCHES = ['master'] RELEASE_BRANCHES = ['release3-staging', 'release3', 'nightly'] + RELEASE_SP_BRANCHES TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] + TESTED_SP_BRANCHES 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: @@ -83,6 +83,10 @@ class OpenpilotMetadata: # touch this to get rid of the orange startup alert. there's better ways to do that return self.git_normalized_origin == "github.com/commaai/openpilot" + @property + def sunnypilot_remote(self) -> bool: + return self.git_normalized_origin == "github.com/sunnypilot/sunnypilot" + @property def git_normalized_origin(self) -> str: return self.git_origin \ @@ -117,9 +121,13 @@ class BuildMetadata: def master_channel(self) -> bool: return self.channel in MASTER_SP_BRANCHES + @property + def development_channel(self) -> bool: + return self.channel.startswith("dev-") or self.channel.endswith("-prebuilt") + @property def channel_type(self) -> str: - if self.channel.startswith("dev-"): + if self.development_channel: return "development" elif self.channel.startswith("staging-"): return "staging" diff --git a/third_party/mapd_pfeiferj/mapd b/third_party/mapd_pfeiferj/mapd index d24d9b757f..e5e45e2b98 100755 Binary files a/third_party/mapd_pfeiferj/mapd and b/third_party/mapd_pfeiferj/mapd differ 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/tinygrad_repo b/tinygrad_repo index 519dec6677..d2bb1bcb97 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 519dec6677f98718ee4f2d07be1936eb91dde73b +Subproject commit d2bb1bcb976f106a41928f2d66d354ab7afd6f59 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..19ef77e01c 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" +cask "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/lib/ui_helpers.py b/tools/replay/lib/ui_helpers.py index 39431eca10..b90cbd93b0 100644 --- a/tools/replay/lib/ui_helpers.py +++ b/tools/replay/lib/ui_helpers.py @@ -198,15 +198,12 @@ def maybe_update_radar_points(lt, lid_overlay): ar_pts = {} for track in lt: ar_pts[track.trackId] = [track.dRel, track.yRel, track.vRel, track.aRel] - for ids, pt in ar_pts.items(): + for pt in ar_pts.values(): # negative here since radar is left positive px, py = to_topdown_pt(pt[0], -pt[1]) if px != -1: - color = 255 - if int(ids) == 1: - lid_overlay[px - 2:px + 2, py - 10:py + 10] = 100 - else: - lid_overlay[px - 2:px + 2, py - 2:py + 2] = color + lid_overlay[px - 4:px + 4, py - 4:py + 4] = 0 + lid_overlay[px - 2:px + 2, py - 2:py + 2] = 255 def get_blank_lid_overlay(UP): lid_overlay = np.zeros((UP.lidar_x, UP.lidar_y), 'uint8') 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/extract_audio.py b/tools/scripts/extract_audio.py new file mode 100755 index 0000000000..2a6ad9f9e3 --- /dev/null +++ b/tools/scripts/extract_audio.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +import os +import sys +import wave +import argparse +import numpy as np + +from openpilot.tools.lib.logreader import LogReader, ReadMode + + +def extract_audio(route_or_segment_name, output_file=None, play=False): + lr = LogReader(route_or_segment_name, default_mode=ReadMode.AUTO_INTERACTIVE) + audio_messages = list(lr.filter("rawAudioData")) + if not audio_messages: + print("No rawAudioData messages found in logs") + return + sample_rate = audio_messages[0].sampleRate + + audio_chunks = [] + total_frames = 0 + for msg in audio_messages: + audio_array = np.frombuffer(msg.data, dtype=np.int16) + audio_chunks.append(audio_array) + total_frames += len(audio_array) + full_audio = np.concatenate(audio_chunks) + + print(f"Found {total_frames} frames from {len(audio_messages)} audio messages at {sample_rate} Hz") + + if output_file: + if write_wav_file(output_file, full_audio, sample_rate): + print(f"Audio written to {output_file}") + else: + print("Audio extraction canceled.") + if play: + play_audio(full_audio, sample_rate) + + +def write_wav_file(filename, audio_data, sample_rate): + if os.path.exists(filename): + if input(f"File '{filename}' exists. Overwrite? (y/N): ").lower() not in ['y', 'yes']: + return False + + with wave.open(filename, 'wb') as wav_file: + wav_file.setnchannels(1) # Mono + wav_file.setsampwidth(2) # 16-bit + wav_file.setframerate(sample_rate) + wav_file.writeframes(audio_data.tobytes()) + return True + + +def play_audio(audio_data, sample_rate): + try: + import sounddevice as sd + + print("Playing audio... Press Ctrl+C to stop") + sd.play(audio_data, sample_rate) + sd.wait() + except KeyboardInterrupt: + print("\nPlayback stopped") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Extract audio data from openpilot logs") + parser.add_argument("-o", "--output", help="Output WAV file path") + parser.add_argument("--play", action="store_true", help="Play audio with sounddevice") + parser.add_argument("route_or_segment_name", nargs='?', help="The route or segment name") + + if len(sys.argv) == 1: + parser.print_help() + sys.exit() + args = parser.parse_args() + + output_file = args.output + if not args.output and not args.play: + output_file = "extracted_audio.wav" + + extract_audio(args.route_or_segment_name.strip(), output_file, args.play) 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/tools/tuning/measure_steering_accuracy.py b/tools/tuning/measure_steering_accuracy.py index a2f437819f..7e4e975742 100755 --- a/tools/tuning/measure_steering_accuracy.py +++ b/tools/tuning/measure_steering_accuracy.py @@ -49,7 +49,7 @@ class SteeringAccuracyTool: active = sm['controlsState'].active steer = sm['carOutput'].actuatorsOutput.torque standstill = sm['carState'].standstill - steer_limited_by_controls = abs(sm['carControl'].actuators.torque - sm['carControl'].actuatorsOutput.torque) > 1e-2 + steer_limited_by_safety = abs(sm['carControl'].actuators.torque - sm['carControl'].actuatorsOutput.torque) > 1e-2 overriding = sm['carState'].steeringPressed changing_lanes = sm['modelV2'].meta.laneChangeState != 0 model_points = sm['modelV2'].position.y @@ -77,7 +77,7 @@ class SteeringAccuracyTool: self.speed_group_stats[group][angle_abs]["steer"] += abs(steer) if len(model_points): self.speed_group_stats[group][angle_abs]["dpp"] += abs(model_points[0]) - if steer_limited_by_controls: + if steer_limited_by_safety: self.speed_group_stats[group][angle_abs]["limited"] += 1 if control_state.saturated: self.speed_group_stats[group][angle_abs]["saturated"] += 1 diff --git a/tools/webcam/camera.py b/tools/webcam/camera.py index 45f9379038..7926766c6f 100644 --- a/tools/webcam/camera.py +++ b/tools/webcam/camera.py @@ -11,7 +11,14 @@ class Camera: self.stream_type = stream_type self.cur_frame_id = 0 + print(f"Opening {cam_type_state} at {camera_id}") + self.cap = cv.VideoCapture(camera_id) + + self.cap.set(cv.CAP_PROP_FRAME_WIDTH, 1280.0) + self.cap.set(cv.CAP_PROP_FRAME_HEIGHT, 720.0) + self.cap.set(cv.CAP_PROP_FPS, 25.0) + self.W = self.cap.get(cv.CAP_PROP_FRAME_WIDTH) self.H = self.cap.get(cv.CAP_PROP_FRAME_HEIGHT) @@ -25,6 +32,8 @@ class Camera: ret, frame = self.cap.read() if not ret: break + # Rotate the frame 180 degrees (flip both axes) + frame = cv.flip(frame, -1) yuv = Camera.bgr2nv12(frame) yield yuv.data.tobytes() self.cap.release() diff --git a/tools/webcam/camerad.py b/tools/webcam/camerad.py index cb096dc00b..f1e70d948a 100755 --- a/tools/webcam/camerad.py +++ b/tools/webcam/camerad.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import threading import os +import platform from collections import namedtuple from msgq.visionipc import VisionIpcServer, VisionStreamType @@ -30,8 +31,7 @@ class Camerad: self.cameras = [] for c in CAMERAS: - cam_device = f"/dev/video{c.cam_id}" - print(f"opening {c.msg_name} at {cam_device}") + cam_device = f"/dev/video{c.cam_id}" if platform.system() != "Darwin" else c.cam_id cam = Camera(c.msg_name, c.stream_type, cam_device) self.cameras.append(cam) self.vipc_server.create_buffers(c.stream_type, 20, cam.W, cam.H) diff --git a/uv.lock b/uv.lock index 530db4ec9d..3b1baa3ee3 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11, <3.13" resolution-markers = [ "python_full_version >= '3.12' and sys_platform == 'darwin'", @@ -21,7 +21,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.12.13" +version = "3.12.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -32,42 +32,42 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160, upload-time = "2025-06-14T15:15:41.354Z" } +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/6a/65/5566b49553bf20ffed6041c665a5504fb047cefdef1b701407b8ce1a47c4/aiohttp-3.12.13-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c229b1437aa2576b99384e4be668af1db84b31a45305d02f61f5497cfa6f60c", size = 709401, upload-time = "2025-06-14T15:13:30.774Z" }, - { url = "https://files.pythonhosted.org/packages/14/b5/48e4cc61b54850bdfafa8fe0b641ab35ad53d8e5a65ab22b310e0902fa42/aiohttp-3.12.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04076d8c63471e51e3689c93940775dc3d12d855c0c80d18ac5a1c68f0904358", size = 481669, upload-time = "2025-06-14T15:13:32.316Z" }, - { url = "https://files.pythonhosted.org/packages/04/4f/e3f95c8b2a20a0437d51d41d5ccc4a02970d8ad59352efb43ea2841bd08e/aiohttp-3.12.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55683615813ce3601640cfaa1041174dc956d28ba0511c8cbd75273eb0587014", size = 469933, upload-time = "2025-06-14T15:13:34.104Z" }, - { url = "https://files.pythonhosted.org/packages/41/c9/c5269f3b6453b1cfbd2cfbb6a777d718c5f086a3727f576c51a468b03ae2/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:921bc91e602d7506d37643e77819cb0b840d4ebb5f8d6408423af3d3bf79a7b7", size = 1740128, upload-time = "2025-06-14T15:13:35.604Z" }, - { url = "https://files.pythonhosted.org/packages/6f/49/a3f76caa62773d33d0cfaa842bdf5789a78749dbfe697df38ab1badff369/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e72d17fe0974ddeae8ed86db297e23dba39c7ac36d84acdbb53df2e18505a013", size = 1688796, upload-time = "2025-06-14T15:13:37.125Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e4/556fccc4576dc22bf18554b64cc873b1a3e5429a5bdb7bbef7f5d0bc7664/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0653d15587909a52e024a261943cf1c5bdc69acb71f411b0dd5966d065a51a47", size = 1787589, upload-time = "2025-06-14T15:13:38.745Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3d/d81b13ed48e1a46734f848e26d55a7391708421a80336e341d2aef3b6db2/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a77b48997c66722c65e157c06c74332cdf9c7ad00494b85ec43f324e5c5a9b9a", size = 1826635, upload-time = "2025-06-14T15:13:40.733Z" }, - { url = "https://files.pythonhosted.org/packages/75/a5/472e25f347da88459188cdaadd1f108f6292f8a25e62d226e63f860486d1/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6946bae55fd36cfb8e4092c921075cde029c71c7cb571d72f1079d1e4e013bc", size = 1729095, upload-time = "2025-06-14T15:13:42.312Z" }, - { url = "https://files.pythonhosted.org/packages/b9/fe/322a78b9ac1725bfc59dfc301a5342e73d817592828e4445bd8f4ff83489/aiohttp-3.12.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f95db8c8b219bcf294a53742c7bda49b80ceb9d577c8e7aa075612b7f39ffb7", size = 1666170, upload-time = "2025-06-14T15:13:44.884Z" }, - { url = "https://files.pythonhosted.org/packages/7a/77/ec80912270e231d5e3839dbd6c065472b9920a159ec8a1895cf868c2708e/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03d5eb3cfb4949ab4c74822fb3326cd9655c2b9fe22e4257e2100d44215b2e2b", size = 1714444, upload-time = "2025-06-14T15:13:46.401Z" }, - { url = "https://files.pythonhosted.org/packages/21/b2/fb5aedbcb2b58d4180e58500e7c23ff8593258c27c089abfbcc7db65bd40/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6383dd0ffa15515283c26cbf41ac8e6705aab54b4cbb77bdb8935a713a89bee9", size = 1709604, upload-time = "2025-06-14T15:13:48.377Z" }, - { url = "https://files.pythonhosted.org/packages/e3/15/a94c05f7c4dc8904f80b6001ad6e07e035c58a8ebfcc15e6b5d58500c858/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6548a411bc8219b45ba2577716493aa63b12803d1e5dc70508c539d0db8dbf5a", size = 1689786, upload-time = "2025-06-14T15:13:50.401Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fd/0d2e618388f7a7a4441eed578b626bda9ec6b5361cd2954cfc5ab39aa170/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81b0fcbfe59a4ca41dc8f635c2a4a71e63f75168cc91026c61be665945739e2d", size = 1783389, upload-time = "2025-06-14T15:13:51.945Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6b/6986d0c75996ef7e64ff7619b9b7449b1d1cbbe05c6755e65d92f1784fe9/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a83797a0174e7995e5edce9dcecc517c642eb43bc3cba296d4512edf346eee2", size = 1803853, upload-time = "2025-06-14T15:13:53.533Z" }, - { url = "https://files.pythonhosted.org/packages/21/65/cd37b38f6655d95dd07d496b6d2f3924f579c43fd64b0e32b547b9c24df5/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5734d8469a5633a4e9ffdf9983ff7cdb512524645c7a3d4bc8a3de45b935ac3", size = 1716909, upload-time = "2025-06-14T15:13:55.148Z" }, - { url = "https://files.pythonhosted.org/packages/fd/20/2de7012427dc116714c38ca564467f6143aec3d5eca3768848d62aa43e62/aiohttp-3.12.13-cp311-cp311-win32.whl", hash = "sha256:fef8d50dfa482925bb6b4c208b40d8e9fa54cecba923dc65b825a72eed9a5dbd", size = 427036, upload-time = "2025-06-14T15:13:57.076Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b6/98518bcc615ef998a64bef371178b9afc98ee25895b4f476c428fade2220/aiohttp-3.12.13-cp311-cp311-win_amd64.whl", hash = "sha256:9a27da9c3b5ed9d04c36ad2df65b38a96a37e9cfba6f1381b842d05d98e6afe9", size = 451427, upload-time = "2025-06-14T15:13:58.505Z" }, - { url = "https://files.pythonhosted.org/packages/b4/6a/ce40e329788013cd190b1d62bbabb2b6a9673ecb6d836298635b939562ef/aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73", size = 700491, upload-time = "2025-06-14T15:14:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/28/d9/7150d5cf9163e05081f1c5c64a0cdf3c32d2f56e2ac95db2a28fe90eca69/aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347", size = 475104, upload-time = "2025-06-14T15:14:01.691Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/d42ba4aed039ce6e449b3e2db694328756c152a79804e64e3da5bc19dffc/aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f", size = 467948, upload-time = "2025-06-14T15:14:03.561Z" }, - { url = "https://files.pythonhosted.org/packages/99/3b/06f0a632775946981d7c4e5a865cddb6e8dfdbaed2f56f9ade7bb4a1039b/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6", size = 1714742, upload-time = "2025-06-14T15:14:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/92/a6/2552eebad9ec5e3581a89256276009e6a974dc0793632796af144df8b740/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5", size = 1697393, upload-time = "2025-06-14T15:14:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/d8/9f/bd08fdde114b3fec7a021381b537b21920cdd2aa29ad48c5dffd8ee314f1/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b", size = 1752486, upload-time = "2025-06-14T15:14:08.808Z" }, - { url = "https://files.pythonhosted.org/packages/f7/e1/affdea8723aec5bd0959171b5490dccd9a91fcc505c8c26c9f1dca73474d/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75", size = 1798643, upload-time = "2025-06-14T15:14:10.767Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9d/666d856cc3af3a62ae86393baa3074cc1d591a47d89dc3bf16f6eb2c8d32/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6", size = 1718082, upload-time = "2025-06-14T15:14:12.38Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ce/3c185293843d17be063dada45efd2712bb6bf6370b37104b4eda908ffdbd/aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8", size = 1633884, upload-time = "2025-06-14T15:14:14.415Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5b/f3413f4b238113be35dfd6794e65029250d4b93caa0974ca572217745bdb/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710", size = 1694943, upload-time = "2025-06-14T15:14:16.48Z" }, - { url = "https://files.pythonhosted.org/packages/82/c8/0e56e8bf12081faca85d14a6929ad5c1263c146149cd66caa7bc12255b6d/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462", size = 1716398, upload-time = "2025-06-14T15:14:18.589Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f3/33192b4761f7f9b2f7f4281365d925d663629cfaea093a64b658b94fc8e1/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae", size = 1657051, upload-time = "2025-06-14T15:14:20.223Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0b/26ddd91ca8f84c48452431cb4c5dd9523b13bc0c9766bda468e072ac9e29/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e", size = 1736611, upload-time = "2025-06-14T15:14:21.988Z" }, - { url = "https://files.pythonhosted.org/packages/c3/8d/e04569aae853302648e2c138a680a6a2f02e374c5b6711732b29f1e129cc/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a", size = 1764586, upload-time = "2025-06-14T15:14:23.979Z" }, - { url = "https://files.pythonhosted.org/packages/ac/98/c193c1d1198571d988454e4ed75adc21c55af247a9fda08236602921c8c8/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5", size = 1724197, upload-time = "2025-06-14T15:14:25.692Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9e/07bb8aa11eec762c6b1ff61575eeeb2657df11ab3d3abfa528d95f3e9337/aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf", size = 421771, upload-time = "2025-06-14T15:14:27.364Z" }, - { url = "https://files.pythonhosted.org/packages/52/66/3ce877e56ec0813069cdc9607cd979575859c597b6fb9b4182c6d5f31886/aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e", size = 447869, upload-time = "2025-06-14T15:14:29.05Z" }, + { 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]] @@ -110,14 +110,15 @@ wheels = [ [[package]] name = "aiosignal" -version = "1.3.2" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424, upload-time = "2024-12-13T17:10:40.86Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] [[package]] @@ -151,21 +152,21 @@ wheels = [ [[package]] name = "azure-core" -version = "1.34.0" +version = "1.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, { name = "six" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/29/ff7a519a315e41c85bab92a7478c6acd1cf0b14353139a08caee4c691f77/azure_core-1.34.0.tar.gz", hash = "sha256:bdb544989f246a0ad1c85d72eeb45f2f835afdcbc5b45e43f0dbde7461c81ece", size = 297999, upload-time = "2025-05-01T23:17:27.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/89/f53968635b1b2e53e4aad2dd641488929fef4ca9dfb0b97927fa7697ddf3/azure_core-1.35.0.tar.gz", hash = "sha256:c0be528489485e9ede59b6971eb63c1eaacf83ef53001bfe3904e475e972be5c", size = 339689, upload-time = "2025-07-03T00:55:23.496Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/9e/5c87b49f65bb16571599bc789857d0ded2f53014d3392bc88a5d1f3ad779/azure_core-1.34.0-py3-none-any.whl", hash = "sha256:0615d3b756beccdb6624d1c0ae97284f38b78fb59a2a9839bf927c66fbbdddd6", size = 207409, upload-time = "2025-05-01T23:17:29.818Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/bf94897361fdd650850f0f2e405b2293e2f12808239046232bdedf554301/azure_core-1.35.0-py3-none-any.whl", hash = "sha256:8db78c72868a58f3de8991eb4d22c4d368fae226dac1002998d6c50437e7dad1", size = 210708, upload-time = "2025-07-03T00:55:25.238Z" }, ] [[package]] name = "azure-identity" -version = "1.23.0" +version = "1.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -174,14 +175,14 @@ dependencies = [ { name = "msal-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/52/458c1be17a5d3796570ae2ed3c6b7b55b134b22d5ef8132b4f97046a9051/azure_identity-1.23.0.tar.gz", hash = "sha256:d9cdcad39adb49d4bb2953a217f62aec1f65bbb3c63c9076da2be2a47e53dde4", size = 265280, upload-time = "2025-05-14T00:18:30.408Z" } +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/07/16/a51d47780f41e4b87bb2d454df6aea90a44a346e918ac189d3700f3d728d/azure_identity-1.23.0-py3-none-any.whl", hash = "sha256:dbbeb64b8e5eaa81c44c565f264b519ff2de7ff0e02271c49f3cb492762a50b0", size = 186097, upload-time = "2025-05-14T00:18:32.734Z" }, + { 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]] name = "azure-storage-blob" -version = "12.25.1" +version = "12.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -189,41 +190,41 @@ dependencies = [ { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/f764536c25cc3829d36857167f03933ce9aee2262293179075439f3cd3ad/azure_storage_blob-12.25.1.tar.gz", hash = "sha256:4f294ddc9bc47909ac66b8934bd26b50d2000278b10ad82cc109764fdc6e0e3b", size = 570541, upload-time = "2025-03-27T17:13:05.424Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/95/3e3414491ce45025a1cde107b6ae72bf72049e6021597c201cd6a3029b9a/azure_storage_blob-12.26.0.tar.gz", hash = "sha256:5dd7d7824224f7de00bfeb032753601c982655173061e242f13be6e26d78d71f", size = 583332, upload-time = "2025-07-16T21:34:07.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/33/085d9352d416e617993821b9d9488222fbb559bc15c3641d6cbd6d16d236/azure_storage_blob-12.25.1-py3-none-any.whl", hash = "sha256:1f337aab12e918ec3f1b638baada97550673911c4ceed892acc8e4e891b74167", size = 406990, upload-time = "2025-03-27T17:13:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/5b/64/63dbfdd83b31200ac58820a7951ddfdeed1fbee9285b0f3eae12d1357155/azure_storage_blob-12.26.0-py3-none-any.whl", hash = "sha256:8c5631b8b22b4f53ec5fff2f3bededf34cfef111e2af613ad42c9e6de00a77fe", size = 412907, upload-time = "2025-07-16T21:34:09.367Z" }, ] [[package]] name = "casadi" -version = "3.7.0" +version = "3.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/49/f8b8eb7c8e98e28e0cca1b41988932025d59a602fb2075f737cbaecf764d/casadi-3.7.0.tar.gz", hash = "sha256:21254f17eb5551c4a938641cc1b815ff3da27271ab2c36e44a3e90ec50ba471f", size = 6027731, upload-time = "2025-03-29T22:31:31.309Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/21/1b4ea75dbddfcdbc7cd70d83147dde72899667097c0c05b288fd7015ef10/casadi-3.7.1.tar.gz", hash = "sha256:12577155cd3cd79ba162381bfed6add1541bc770ba3f1f1334e4eb159d9b39ba", size = 6065586, upload-time = "2025-07-23T21:47:56.483Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/cf/7d1dc6b16f690f1fac22a0a43684de3a03b3f631a1d8c25393f320b5f971/casadi-3.7.0-cp311-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:50bba75bb9575baa09a2e25ce3c1968803320c49b9dcc218139321dab02f0400", size = 48302536, upload-time = "2025-03-30T07:31:37.067Z" }, - { url = "https://files.pythonhosted.org/packages/74/d2/f69156aaf4545543995c791a0f97551b66f9c9bc8d92361973ee6378ad25/casadi-3.7.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:8f138bf370603f6f820385d78882f840168e8e0866bea5de08b6b54a6e22c093", size = 44713683, upload-time = "2025-03-30T07:33:37.446Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3b/13a7d92c27543e0a2eb559f2a5acc7278f1a2ef0217772b09df50ac1c2dc/casadi-3.7.0-cp311-none-manylinux2014_aarch64.whl", hash = "sha256:4d844a1281c7ff527115d27ee8f2c58315d0008cac835656323a6d502de27010", size = 45518255, upload-time = "2025-03-30T07:36:03.844Z" }, - { url = "https://files.pythonhosted.org/packages/bb/88/00adcb26a0c313ebeaa2db8b2691e37418d6b7c3e06b4a90a3ac9380b91b/casadi-3.7.0-cp311-none-manylinux2014_i686.whl", hash = "sha256:b8640486db98b75a75eb05b6191d5a7f0d44774c36c07c7da327d4d740914284", size = 74329105, upload-time = "2025-03-30T07:39:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/64/b31fd4ce5b93f97fd16a9ba7ce8d4a8d36334a518f1ad00525340db31ff4/casadi-3.7.0-cp311-none-manylinux2014_x86_64.whl", hash = "sha256:65d9a61660c75ff8f0c30abf52e2f9cefcdee1cbb9301e26678f64116f509ee7", size = 77330636, upload-time = "2025-03-30T07:46:55.954Z" }, - { url = "https://files.pythonhosted.org/packages/c4/25/79bb6b866a7005186ccdfcb89e01032885a9efa90acfe2d079253685622b/casadi-3.7.0-cp311-none-win_amd64.whl", hash = "sha256:391b5ee886cde28bf813e820958bfcdef98314bd367a93c95e7bef1bf713b886", size = 50949252, upload-time = "2025-03-30T07:49:05.348Z" }, - { url = "https://files.pythonhosted.org/packages/b8/27/6ca4c831d9131eb15ca34346398c1379a577363c264ad47983c1be65b3e1/casadi-3.7.0-cp312-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:ceb4d67c1c5cdf80f233866fcdcaf7d77523115983081fe909b70fe89fd7b708", size = 48303214, upload-time = "2025-03-30T07:51:05.246Z" }, - { url = "https://files.pythonhosted.org/packages/b2/cb/cd8be8aeac6453e2aad96793101fa287f54f86b643d542375969738930c3/casadi-3.7.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:83dbc27125044e7600b04af648f24e6924d46e81afe6ff088218e4b7e9a37f08", size = 44713758, upload-time = "2025-03-30T07:52:44.766Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3d/c8a392eac772b1537f63ee16b57be87bcdef9d9bc9530b54c95b1a122960/casadi-3.7.0-cp312-none-manylinux2014_aarch64.whl", hash = "sha256:53731171b41ca0640b76657d2320b929634a137ff30f12f985196df35411885b", size = 45517623, upload-time = "2025-03-30T07:54:25.318Z" }, - { url = "https://files.pythonhosted.org/packages/68/d2/2acd3b8cf8fa25bca342cd539f69dc94b0fa1bf3acaa30f13848fa0f31ee/casadi-3.7.0-cp312-none-manylinux2014_i686.whl", hash = "sha256:6b30fb73d8c140fbbe51e60d00412aaefe5a7b775257a422ea244f423bd2351c", size = 74319729, upload-time = "2025-03-30T07:57:18.329Z" }, - { url = "https://files.pythonhosted.org/packages/0f/35/ec351423c854a74884218501a431e018c6eee79461dde8e91f9ce6b4e2b5/casadi-3.7.0-cp312-none-manylinux2014_x86_64.whl", hash = "sha256:b795371bf09ec0bfd22eaa0a1e81ce2cd3ecd811c9d370b98f6853b87e8397c9", size = 77323153, upload-time = "2025-03-30T08:00:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4b/ab605b5948795fe15b5930ecc5ac7ba72ebad116a9003c0117421cbe34a8/casadi-3.7.0-cp312-none-win_amd64.whl", hash = "sha256:e77173aad67b817ebf4320c31cef37c7d666f9895bc5970f3370b2ae6fdad587", size = 50946897, upload-time = "2025-03-30T08:02:37.836Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4d/cfe092b2deb65dbf913d38148e30dfe41bbe9f289f892ea7a5d3526532d2/casadi-3.7.1-cp311-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:c13fafdff0cdf73392fe2002245befe7496b5ac99b70adb64babff0099068c8a", size = 47100775, upload-time = "2025-07-23T19:56:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/13/41/c4ad627e447a083fbfd2826f1f4684223f0072fdc49f58157ca6a6ce0a77/casadi-3.7.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:74d9c6bad3c7cb347494d52cc3e6faf3fbf3300046fab0e1ddc9439d3fd68486", size = 42282155, upload-time = "2025-07-23T19:59:11.191Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e2/53733933d454657e319cc8074e004ec46660c4797cbffcaf7695a874b0da/casadi-3.7.1-cp311-none-manylinux2014_aarch64.whl", hash = "sha256:6fbc0803e6c7ffb9ba9a41ba38e7c1dadee73835edbacacce3530343f078f3f9", size = 47272638, upload-time = "2025-07-23T20:03:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/e1/72/8002d4fe64325842eb6a7f7ac1e4574c02584e85e5b9d93a08a789095505/casadi-3.7.1-cp311-none-manylinux2014_i686.whl", hash = "sha256:38a16b0c7caff5297df91c7e3a65091824979367e6fc8e6eb0e5ab9f1f832af8", size = 72418203, upload-time = "2025-07-23T20:07:18.681Z" }, + { url = "https://files.pythonhosted.org/packages/96/e5/065287b78c4f6a00a010d7c3f5316df1b95f7c5bda644f2151b54a79a964/casadi-3.7.1-cp311-none-manylinux2014_x86_64.whl", hash = "sha256:2dd75cc1b5ca8009586186e4a63e0cdd83a672bf308edc75ef84f9c896cd971d", size = 75558461, upload-time = "2025-07-23T21:33:20.771Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4c/084d2f3bca33d0b3d509be17c3b678f6bcc03f3409d42e88750cb1e44966/casadi-3.7.1-cp311-none-win_amd64.whl", hash = "sha256:3ab68c6dd621b5f150635bf06aa8e1697cae2fbe0137e9d47270c54d305c334c", size = 50987255, upload-time = "2025-07-23T21:34:20.349Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a3/65942d3e0f7589a72e69dd9dee575963ecf51504e985f132bce8b0d24988/casadi-3.7.1-cp312-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:4a29ae3212b0ae6931d439ca3bfa2ac279826e99c2c9a443704e84021933ab76", size = 47102059, upload-time = "2025-07-23T20:36:47.174Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f4/dc371199583f6b154ba0966c4132418737a2d2e019c9f6ad227ebd279684/casadi-3.7.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:70edf1df3fc83401eb5a2f7053ae8394f4e612e61c2ad6de088e3d76d0ec845e", size = 42282702, upload-time = "2025-07-23T20:39:41.499Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ff/5f8a9378cd7b322d1dcc8b8aa9fbc454ad57152b754c94b50ffdc67254a0/casadi-3.7.1-cp312-none-manylinux2014_aarch64.whl", hash = "sha256:a1717bbd07a6eb98acddf5ea5da7e78a9b85b75dbabb55190cc6b0f2812ef39d", size = 47271636, upload-time = "2025-07-23T20:42:59.961Z" }, + { url = "https://files.pythonhosted.org/packages/e1/11/e1b6a76bf288fc86dc8919cbcd2d3a654e7ead4240d9928deb8ed4ee91da/casadi-3.7.1-cp312-none-manylinux2014_i686.whl", hash = "sha256:57ae29d16af0716ebc15d161bcf0bcd97d35631b10c0e80838290d249cac80d7", size = 72410197, upload-time = "2025-07-23T21:35:35.509Z" }, + { url = "https://files.pythonhosted.org/packages/c1/76/bb43ff625066b178132172d41db0c5c962b4af9db15c88b8a23e65376d18/casadi-3.7.1-cp312-none-manylinux2014_x86_64.whl", hash = "sha256:756f3d07d3414dc54f05ebec883ac3db6e307bfe661719d188340504b6bed420", size = 75552890, upload-time = "2025-07-23T21:37:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/11/0c/8026b0b238e00a2e63c7142505cbcd0931b4db4140d55c83962f9d8e0b02/casadi-3.7.1-cp312-none-win_amd64.whl", hash = "sha256:b228fbcd1f41eb8d5405dbc1e0ecb780daf89808392706bb77fd2b240f8a437e", size = 50984453, upload-time = "2025-07-23T21:38:18.21Z" }, ] [[package]] name = "certifi" -version = "2025.6.15" +version = "2025.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +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/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, + { 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]] @@ -262,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]] @@ -336,73 +333,40 @@ wheels = [ [[package]] name = "contourpy" -version = "1.3.2" +version = "1.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, - { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, - { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, - { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, - { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, - { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, - { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, - { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, - { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, - { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, -] - -[[package]] -name = "coverage" -version = "7.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/e0/98670a80884f64578f0c22cd70c5e81a6e07b08167721c7487b4d70a7ca0/coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec", size = 813650, upload-time = "2025-06-13T13:02:28.627Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/34/fa69372a07d0903a78ac103422ad34db72281c9fc625eba94ac1185da66f/coverage-7.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95c765060e65c692da2d2f51a9499c5e9f5cf5453aeaf1420e3fc847cc060582", size = 212146, upload-time = "2025-06-13T13:00:48.496Z" }, - { url = "https://files.pythonhosted.org/packages/27/f0/da1894915d2767f093f081c42afeba18e760f12fdd7a2f4acbe00564d767/coverage-7.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba383dc6afd5ec5b7a0d0c23d38895db0e15bcba7fb0fa8901f245267ac30d86", size = 212536, upload-time = "2025-06-13T13:00:51.535Z" }, - { url = "https://files.pythonhosted.org/packages/10/d5/3fc33b06e41e390f88eef111226a24e4504d216ab8e5d1a7089aa5a3c87a/coverage-7.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae0383f13cbdcf1e5e7014489b0d71cc0106458878ccde52e8a12ced4298ed", size = 245092, upload-time = "2025-06-13T13:00:52.883Z" }, - { url = "https://files.pythonhosted.org/packages/0a/39/7aa901c14977aba637b78e95800edf77f29f5a380d29768c5b66f258305b/coverage-7.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69aa417a030bf11ec46149636314c24c8d60fadb12fc0ee8f10fda0d918c879d", size = 242806, upload-time = "2025-06-13T13:00:54.571Z" }, - { url = "https://files.pythonhosted.org/packages/43/fc/30e5cfeaf560b1fc1989227adedc11019ce4bb7cce59d65db34fe0c2d963/coverage-7.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4be2a28656afe279b34d4f91c3e26eccf2f85500d4a4ff0b1f8b54bf807338", size = 244610, upload-time = "2025-06-13T13:00:56.932Z" }, - { url = "https://files.pythonhosted.org/packages/bf/15/cca62b13f39650bc87b2b92bb03bce7f0e79dd0bf2c7529e9fc7393e4d60/coverage-7.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:382e7ddd5289f140259b610e5f5c58f713d025cb2f66d0eb17e68d0a94278875", size = 244257, upload-time = "2025-06-13T13:00:58.545Z" }, - { url = "https://files.pythonhosted.org/packages/cd/1a/c0f2abe92c29e1464dbd0ff9d56cb6c88ae2b9e21becdb38bea31fcb2f6c/coverage-7.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e5532482344186c543c37bfad0ee6069e8ae4fc38d073b8bc836fc8f03c9e250", size = 242309, upload-time = "2025-06-13T13:00:59.836Z" }, - { url = "https://files.pythonhosted.org/packages/57/8d/c6fd70848bd9bf88fa90df2af5636589a8126d2170f3aade21ed53f2b67a/coverage-7.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a39d18b3f50cc121d0ce3838d32d58bd1d15dab89c910358ebefc3665712256c", size = 242898, upload-time = "2025-06-13T13:01:02.506Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9e/6ca46c7bff4675f09a66fe2797cd1ad6a24f14c9c7c3b3ebe0470a6e30b8/coverage-7.9.1-cp311-cp311-win32.whl", hash = "sha256:dd24bd8d77c98557880def750782df77ab2b6885a18483dc8588792247174b32", size = 214561, upload-time = "2025-06-13T13:01:04.012Z" }, - { url = "https://files.pythonhosted.org/packages/a1/30/166978c6302010742dabcdc425fa0f938fa5a800908e39aff37a7a876a13/coverage-7.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:6b55ad10a35a21b8015eabddc9ba31eb590f54adc9cd39bcf09ff5349fd52125", size = 215493, upload-time = "2025-06-13T13:01:05.702Z" }, - { url = "https://files.pythonhosted.org/packages/60/07/a6d2342cd80a5be9f0eeab115bc5ebb3917b4a64c2953534273cf9bc7ae6/coverage-7.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ad935f0016be24c0e97fc8c40c465f9c4b85cbbe6eac48934c0dc4d2568321e", size = 213869, upload-time = "2025-06-13T13:01:09.345Z" }, - { url = "https://files.pythonhosted.org/packages/68/d9/7f66eb0a8f2fce222de7bdc2046ec41cb31fe33fb55a330037833fb88afc/coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626", size = 212336, upload-time = "2025-06-13T13:01:10.909Z" }, - { url = "https://files.pythonhosted.org/packages/20/20/e07cb920ef3addf20f052ee3d54906e57407b6aeee3227a9c91eea38a665/coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb", size = 212571, upload-time = "2025-06-13T13:01:12.518Z" }, - { url = "https://files.pythonhosted.org/packages/78/f8/96f155de7e9e248ca9c8ff1a40a521d944ba48bec65352da9be2463745bf/coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300", size = 246377, upload-time = "2025-06-13T13:01:14.87Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cf/1d783bd05b7bca5c10ded5f946068909372e94615a4416afadfe3f63492d/coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8", size = 243394, upload-time = "2025-06-13T13:01:16.23Z" }, - { url = "https://files.pythonhosted.org/packages/02/dd/e7b20afd35b0a1abea09fb3998e1abc9f9bd953bee548f235aebd2b11401/coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5", size = 245586, upload-time = "2025-06-13T13:01:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/4e/38/b30b0006fea9d617d1cb8e43b1bc9a96af11eff42b87eb8c716cf4d37469/coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd", size = 245396, upload-time = "2025-06-13T13:01:19.164Z" }, - { url = "https://files.pythonhosted.org/packages/31/e4/4d8ec1dc826e16791f3daf1b50943e8e7e1eb70e8efa7abb03936ff48418/coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898", size = 243577, upload-time = "2025-06-13T13:01:22.433Z" }, - { url = "https://files.pythonhosted.org/packages/25/f4/b0e96c5c38e6e40ef465c4bc7f138863e2909c00e54a331da335faf0d81a/coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d", size = 244809, upload-time = "2025-06-13T13:01:24.143Z" }, - { url = "https://files.pythonhosted.org/packages/8a/65/27e0a1fa5e2e5079bdca4521be2f5dabf516f94e29a0defed35ac2382eb2/coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74", size = 214724, upload-time = "2025-06-13T13:01:25.435Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a8/d5b128633fd1a5e0401a4160d02fa15986209a9e47717174f99dc2f7166d/coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e", size = 215535, upload-time = "2025-06-13T13:01:27.861Z" }, - { url = "https://files.pythonhosted.org/packages/a3/37/84bba9d2afabc3611f3e4325ee2c6a47cd449b580d4a606b240ce5a6f9bf/coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342", size = 213904, upload-time = "2025-06-13T13:01:29.202Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e5/c723545c3fd3204ebde3b4cc4b927dce709d3b6dc577754bb57f63ca4a4a/coverage-7.9.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514", size = 204009, upload-time = "2025-06-13T13:02:25.787Z" }, - { url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload-time = "2025-06-13T13:02:27.173Z" }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] [[package]] @@ -511,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" }, @@ -546,27 +510,27 @@ wheels = [ [[package]] name = "fonttools" -version = "4.58.4" +version = "4.59.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/5a/1124b2c8cb3a8015faf552e92714040bcdbc145dfa29928891b02d147a18/fonttools-4.58.4.tar.gz", hash = "sha256:928a8009b9884ed3aae17724b960987575155ca23c6f0b8146e400cc9e0d44ba", size = 3525026, upload-time = "2025-06-13T17:25:15.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521, upload-time = "2025-07-16T12:04:54.613Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/7b/cc6e9bb41bab223bd2dc70ba0b21386b85f604e27f4c3206b4205085a2ab/fonttools-4.58.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3841991c9ee2dc0562eb7f23d333d34ce81e8e27c903846f0487da21e0028eb", size = 2768901, upload-time = "2025-06-13T17:24:05.901Z" }, - { url = "https://files.pythonhosted.org/packages/3d/15/98d75df9f2b4e7605f3260359ad6e18e027c11fa549f74fce567270ac891/fonttools-4.58.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c98f91b6a9604e7ffb5ece6ea346fa617f967c2c0944228801246ed56084664", size = 2328696, upload-time = "2025-06-13T17:24:09.18Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c8/dc92b80f5452c9c40164e01b3f78f04b835a00e673bd9355ca257008ff61/fonttools-4.58.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab9f891eb687ddf6a4e5f82901e00f992e18012ca97ab7acd15f13632acd14c1", size = 5018830, upload-time = "2025-06-13T17:24:11.282Z" }, - { url = "https://files.pythonhosted.org/packages/19/48/8322cf177680505d6b0b6062e204f01860cb573466a88077a9b795cb70e8/fonttools-4.58.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:891c5771e8f0094b7c0dc90eda8fc75e72930b32581418f2c285a9feedfd9a68", size = 4960922, upload-time = "2025-06-13T17:24:14.9Z" }, - { url = "https://files.pythonhosted.org/packages/14/e0/2aff149ed7eb0916de36da513d473c6fff574a7146891ce42de914899395/fonttools-4.58.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:43ba4d9646045c375d22e3473b7d82b18b31ee2ac715cd94220ffab7bc2d5c1d", size = 4997135, upload-time = "2025-06-13T17:24:16.959Z" }, - { url = "https://files.pythonhosted.org/packages/e6/6f/4d9829b29a64a2e63a121cb11ecb1b6a9524086eef3e35470949837a1692/fonttools-4.58.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33d19f16e6d2ffd6669bda574a6589941f6c99a8d5cfb9f464038244c71555de", size = 5108701, upload-time = "2025-06-13T17:24:18.849Z" }, - { url = "https://files.pythonhosted.org/packages/6f/1e/2d656ddd1b0cd0d222f44b2d008052c2689e66b702b9af1cd8903ddce319/fonttools-4.58.4-cp311-cp311-win32.whl", hash = "sha256:b59e5109b907da19dc9df1287454821a34a75f2632a491dd406e46ff432c2a24", size = 2200177, upload-time = "2025-06-13T17:24:20.823Z" }, - { url = "https://files.pythonhosted.org/packages/fb/83/ba71ad053fddf4157cb0697c8da8eff6718d059f2a22986fa5f312b49c92/fonttools-4.58.4-cp311-cp311-win_amd64.whl", hash = "sha256:3d471a5b567a0d1648f2e148c9a8bcf00d9ac76eb89e976d9976582044cc2509", size = 2247892, upload-time = "2025-06-13T17:24:22.927Z" }, - { url = "https://files.pythonhosted.org/packages/04/3c/1d1792bfe91ef46f22a3d23b4deb514c325e73c17d4f196b385b5e2faf1c/fonttools-4.58.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:462211c0f37a278494e74267a994f6be9a2023d0557aaa9ecbcbfce0f403b5a6", size = 2754082, upload-time = "2025-06-13T17:24:24.862Z" }, - { url = "https://files.pythonhosted.org/packages/2a/1f/2b261689c901a1c3bc57a6690b0b9fc21a9a93a8b0c83aae911d3149f34e/fonttools-4.58.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c7a12fb6f769165547f00fcaa8d0df9517603ae7e04b625e5acb8639809b82d", size = 2321677, upload-time = "2025-06-13T17:24:26.815Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6b/4607add1755a1e6581ae1fc0c9a640648e0d9cdd6591cc2d581c2e07b8c3/fonttools-4.58.4-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d42c63020a922154add0a326388a60a55504629edc3274bc273cd3806b4659f", size = 4896354, upload-time = "2025-06-13T17:24:28.428Z" }, - { url = "https://files.pythonhosted.org/packages/cd/95/34b4f483643d0cb11a1f830b72c03fdd18dbd3792d77a2eb2e130a96fada/fonttools-4.58.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f2b4e6fd45edc6805f5f2c355590b092ffc7e10a945bd6a569fc66c1d2ae7aa", size = 4941633, upload-time = "2025-06-13T17:24:30.568Z" }, - { url = "https://files.pythonhosted.org/packages/81/ac/9bafbdb7694059c960de523e643fa5a61dd2f698f3f72c0ca18ae99257c7/fonttools-4.58.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f155b927f6efb1213a79334e4cb9904d1e18973376ffc17a0d7cd43d31981f1e", size = 4886170, upload-time = "2025-06-13T17:24:32.724Z" }, - { url = "https://files.pythonhosted.org/packages/ae/44/a3a3b70d5709405f7525bb7cb497b4e46151e0c02e3c8a0e40e5e9fe030b/fonttools-4.58.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e38f687d5de97c7fb7da3e58169fb5ba349e464e141f83c3c2e2beb91d317816", size = 5037851, upload-time = "2025-06-13T17:24:35.034Z" }, - { url = "https://files.pythonhosted.org/packages/21/cb/e8923d197c78969454eb876a4a55a07b59c9c4c46598f02b02411dc3b45c/fonttools-4.58.4-cp312-cp312-win32.whl", hash = "sha256:636c073b4da9db053aa683db99580cac0f7c213a953b678f69acbca3443c12cc", size = 2187428, upload-time = "2025-06-13T17:24:36.996Z" }, - { url = "https://files.pythonhosted.org/packages/46/e6/fe50183b1a0e1018e7487ee740fa8bb127b9f5075a41e20d017201e8ab14/fonttools-4.58.4-cp312-cp312-win_amd64.whl", hash = "sha256:82e8470535743409b30913ba2822e20077acf9ea70acec40b10fcf5671dceb58", size = 2236649, upload-time = "2025-06-13T17:24:38.985Z" }, - { url = "https://files.pythonhosted.org/packages/0b/2f/c536b5b9bb3c071e91d536a4d11f969e911dbb6b227939f4c5b0bca090df/fonttools-4.58.4-py3-none-any.whl", hash = "sha256:a10ce13a13f26cbb9f37512a4346bb437ad7e002ff6fa966a7ce7ff5ac3528bd", size = 1114660, upload-time = "2025-06-13T17:25:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/06/96/520733d9602fa1bf6592e5354c6721ac6fc9ea72bc98d112d0c38b967199/fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c", size = 2782387, upload-time = "2025-07-16T12:03:51.424Z" }, + { url = "https://files.pythonhosted.org/packages/87/6a/170fce30b9bce69077d8eec9bea2cfd9f7995e8911c71be905e2eba6368b/fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5", size = 2342194, upload-time = "2025-07-16T12:03:53.295Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/7c8166c0066856f1408092f7968ac744060cf72ca53aec9036106f57eeca/fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705", size = 5032333, upload-time = "2025-07-16T12:03:55.177Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0c/707c5a19598eafcafd489b73c4cb1c142102d6197e872f531512d084aa76/fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464", size = 4974422, upload-time = "2025-07-16T12:03:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e7/6d33737d9fe632a0f59289b6f9743a86d2a9d0673de2a0c38c0f54729822/fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38", size = 5010631, upload-time = "2025-07-16T12:03:59.449Z" }, + { url = "https://files.pythonhosted.org/packages/63/e1/a4c3d089ab034a578820c8f2dff21ef60daf9668034a1e4fb38bb1cc3398/fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6", size = 5122198, upload-time = "2025-07-16T12:04:01.542Z" }, + { url = "https://files.pythonhosted.org/packages/09/77/ca82b9c12fa4de3c520b7760ee61787640cf3fde55ef1b0bfe1de38c8153/fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757", size = 2214216, upload-time = "2025-07-16T12:04:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/ab/25/5aa7ca24b560b2f00f260acf32c4cf29d7aaf8656e159a336111c18bc345/fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0", size = 2261879, upload-time = "2025-07-16T12:04:05.015Z" }, + { url = "https://files.pythonhosted.org/packages/e2/77/b1c8af22f4265e951cd2e5535dbef8859efcef4fb8dee742d368c967cddb/fonttools-4.59.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b", size = 2767562, upload-time = "2025-07-16T12:04:06.895Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5a/aeb975699588176bb357e8b398dfd27e5d3a2230d92b81ab8cbb6187358d/fonttools-4.59.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2", size = 2335168, upload-time = "2025-07-16T12:04:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/54/97/c6101a7e60ae138c4ef75b22434373a0da50a707dad523dd19a4889315bf/fonttools-4.59.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b", size = 4909850, upload-time = "2025-07-16T12:04:10.761Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6c/fa4d18d641054f7bff878cbea14aa9433f292b9057cb1700d8e91a4d5f4f/fonttools-4.59.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1", size = 4955131, upload-time = "2025-07-16T12:04:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/20/5c/331947fc1377deb928a69bde49f9003364f5115e5cbe351eea99e39412a2/fonttools-4.59.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e", size = 4899667, upload-time = "2025-07-16T12:04:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/8a/46/b66469dfa26b8ff0baa7654b2cc7851206c6d57fe3abdabbaab22079a119/fonttools-4.59.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e", size = 5051349, upload-time = "2025-07-16T12:04:16.388Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ebfb6b1f3a4328ab69787d106a7d92ccde77ce66e98659df0f9e3f28d93d/fonttools-4.59.0-cp312-cp312-win32.whl", hash = "sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b", size = 2201315, upload-time = "2025-07-16T12:04:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/09/45/d2bdc9ea20bbadec1016fd0db45696d573d7a26d95ab5174ffcb6d74340b/fonttools-4.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01", size = 2249408, upload-time = "2025-07-16T12:04:20.489Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050, upload-time = "2025-07-16T12:04:52.687Z" }, ] [[package]] @@ -655,7 +619,7 @@ wheels = [ [[package]] name = "gymnasium" -version = "1.1.1" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloudpickle" }, @@ -663,9 +627,9 @@ dependencies = [ { name = "numpy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/69/70cd29e9fc4953d013b15981ee71d4c9ef4d8b2183e6ef2fe89756746dce/gymnasium-1.1.1.tar.gz", hash = "sha256:8bd9ea9bdef32c950a444ff36afc785e1d81051ec32d30435058953c20d2456d", size = 829326, upload-time = "2025-03-06T16:30:36.428Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/17/c2a0e15c2cd5a8e788389b280996db927b923410de676ec5c7b2695e9261/gymnasium-1.2.0.tar.gz", hash = "sha256:344e87561012558f603880baf264ebc97f8a5c997a957b0c9f910281145534b0", size = 821142, upload-time = "2025-06-27T08:21:20.262Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/68/2bdc7b46b5f543dd865575f9d19716866bdb76e50dd33b71ed1a3dd8bb42/gymnasium-1.1.1-py3-none-any.whl", hash = "sha256:9c167ec0a2b388666e37f63b2849cd2552f7f5b71938574c637bb36487eb928a", size = 965410, upload-time = "2025-03-06T16:30:34.443Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e2/a111dbb8625af467ea4760a1373d6ef27aac3137931219902406ccc05423/gymnasium-1.2.0-py3-none-any.whl", hash = "sha256:fc4a1e4121a9464c29b4d7dc6ade3fbeaa36dea448682f5f71a6d2c17489ea76", size = 944301, upload-time = "2025-06-27T08:21:18.83Z" }, ] [[package]] @@ -749,40 +713,41 @@ wheels = [ [[package]] name = "kiwisolver" -version = "1.4.8" +version = "1.4.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635, upload-time = "2024-12-24T18:28:51.826Z" }, - { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717, upload-time = "2024-12-24T18:28:54.256Z" }, - { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413, upload-time = "2024-12-24T18:28:55.184Z" }, - { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994, upload-time = "2024-12-24T18:28:57.493Z" }, - { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804, upload-time = "2024-12-24T18:29:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690, upload-time = "2024-12-24T18:29:01.401Z" }, - { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839, upload-time = "2024-12-24T18:29:02.685Z" }, - { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109, upload-time = "2024-12-24T18:29:04.113Z" }, - { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269, upload-time = "2024-12-24T18:29:05.488Z" }, - { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468, upload-time = "2024-12-24T18:29:06.79Z" }, - { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394, upload-time = "2024-12-24T18:29:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901, upload-time = "2024-12-24T18:29:09.653Z" }, - { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306, upload-time = "2024-12-24T18:29:12.644Z" }, - { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966, upload-time = "2024-12-24T18:29:14.089Z" }, - { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311, upload-time = "2024-12-24T18:29:15.892Z" }, - { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload-time = "2024-12-24T18:29:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload-time = "2024-12-24T18:29:19.146Z" }, - { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload-time = "2024-12-24T18:29:20.096Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload-time = "2024-12-24T18:29:22.843Z" }, - { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload-time = "2024-12-24T18:29:24.463Z" }, - { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload-time = "2024-12-24T18:29:25.776Z" }, - { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload-time = "2024-12-24T18:29:27.202Z" }, - { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload-time = "2024-12-24T18:29:28.638Z" }, - { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload-time = "2024-12-24T18:29:30.368Z" }, - { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload-time = "2024-12-24T18:29:33.151Z" }, - { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload-time = "2024-12-24T18:29:34.584Z" }, - { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload-time = "2024-12-24T18:29:36.138Z" }, - { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" }, - { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" }, - { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, ] [[package]] @@ -796,64 +761,42 @@ 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 = "5.4.0" +version = "6.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240, upload-time = "2025-04-23T01:45:18.566Z" }, - { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685, upload-time = "2025-04-23T01:45:21.387Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164, upload-time = "2025-04-23T01:45:23.849Z" }, - { url = "https://files.pythonhosted.org/packages/f9/94/bbc66e42559f9d04857071e3b3d0c9abd88579367fd2588a4042f641f57e/lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df", size = 4746206, upload-time = "2025-04-23T01:45:26.361Z" }, - { url = "https://files.pythonhosted.org/packages/66/95/34b0679bee435da2d7cae895731700e519a8dfcab499c21662ebe671603e/lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e", size = 5342144, upload-time = "2025-04-23T01:45:28.939Z" }, - { url = "https://files.pythonhosted.org/packages/e0/5d/abfcc6ab2fa0be72b2ba938abdae1f7cad4c632f8d552683ea295d55adfb/lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44", size = 4825124, upload-time = "2025-04-23T01:45:31.361Z" }, - { url = "https://files.pythonhosted.org/packages/5a/78/6bd33186c8863b36e084f294fc0a5e5eefe77af95f0663ef33809cc1c8aa/lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba", size = 4876520, upload-time = "2025-04-23T01:45:34.191Z" }, - { url = "https://files.pythonhosted.org/packages/3b/74/4d7ad4839bd0fc64e3d12da74fc9a193febb0fae0ba6ebd5149d4c23176a/lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba", size = 4765016, upload-time = "2025-04-23T01:45:36.7Z" }, - { url = "https://files.pythonhosted.org/packages/24/0d/0a98ed1f2471911dadfc541003ac6dd6879fc87b15e1143743ca20f3e973/lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c", size = 5362884, upload-time = "2025-04-23T01:45:39.291Z" }, - { url = "https://files.pythonhosted.org/packages/48/de/d4f7e4c39740a6610f0f6959052b547478107967362e8424e1163ec37ae8/lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8", size = 4902690, upload-time = "2025-04-23T01:45:42.386Z" }, - { url = "https://files.pythonhosted.org/packages/07/8c/61763abd242af84f355ca4ef1ee096d3c1b7514819564cce70fd18c22e9a/lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86", size = 4944418, upload-time = "2025-04-23T01:45:46.051Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c5/6d7e3b63e7e282619193961a570c0a4c8a57fe820f07ca3fe2f6bd86608a/lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056", size = 4827092, upload-time = "2025-04-23T01:45:48.943Z" }, - { url = "https://files.pythonhosted.org/packages/71/4a/e60a306df54680b103348545706a98a7514a42c8b4fbfdcaa608567bb065/lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7", size = 5418231, upload-time = "2025-04-23T01:45:51.481Z" }, - { url = "https://files.pythonhosted.org/packages/27/f2/9754aacd6016c930875854f08ac4b192a47fe19565f776a64004aa167521/lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd", size = 5261798, upload-time = "2025-04-23T01:45:54.146Z" }, - { url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195, upload-time = "2025-04-23T01:45:56.685Z" }, - { url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243, upload-time = "2025-04-23T01:45:58.863Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197, upload-time = "2025-04-23T01:46:01.096Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392, upload-time = "2025-04-23T01:46:04.09Z" }, - { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103, upload-time = "2025-04-23T01:46:07.227Z" }, - { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224, upload-time = "2025-04-23T01:46:10.237Z" }, - { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913, upload-time = "2025-04-23T01:46:12.757Z" }, - { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441, upload-time = "2025-04-23T01:46:16.037Z" }, - { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165, upload-time = "2025-04-23T01:46:19.137Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580, upload-time = "2025-04-23T01:46:21.963Z" }, - { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493, upload-time = "2025-04-23T01:46:24.316Z" }, - { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679, upload-time = "2025-04-23T01:46:27.097Z" }, - { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691, upload-time = "2025-04-23T01:46:30.009Z" }, - { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075, upload-time = "2025-04-23T01:46:32.33Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680, upload-time = "2025-04-23T01:46:34.852Z" }, - { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253, upload-time = "2025-04-23T01:46:37.608Z" }, - { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651, upload-time = "2025-04-23T01:46:40.183Z" }, - { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315, upload-time = "2025-04-23T01:46:43.333Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149, upload-time = "2025-04-23T01:46:45.684Z" }, - { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095, upload-time = "2025-04-23T01:46:48.521Z" }, + { url = "https://files.pythonhosted.org/packages/7c/23/828d4cc7da96c611ec0ce6147bbcea2fdbde023dc995a165afa512399bbf/lxml-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ee56288d0df919e4aac43b539dd0e34bb55d6a12a6562038e8d6f3ed07f9e36", size = 8438217, upload-time = "2025-06-26T16:25:34.349Z" }, + { url = "https://files.pythonhosted.org/packages/f1/33/5ac521212c5bcb097d573145d54b2b4a3c9766cda88af5a0e91f66037c6e/lxml-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8dd6dd0e9c1992613ccda2bcb74fc9d49159dbe0f0ca4753f37527749885c25", size = 4590317, upload-time = "2025-06-26T16:25:38.103Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2e/45b7ca8bee304c07f54933c37afe7dd4d39ff61ba2757f519dcc71bc5d44/lxml-6.0.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:d7ae472f74afcc47320238b5dbfd363aba111a525943c8a34a1b657c6be934c3", size = 5221628, upload-time = "2025-06-26T16:25:40.878Z" }, + { url = "https://files.pythonhosted.org/packages/32/23/526d19f7eb2b85da1f62cffb2556f647b049ebe2a5aa8d4d41b1fb2c7d36/lxml-6.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5592401cdf3dc682194727c1ddaa8aa0f3ddc57ca64fd03226a430b955eab6f6", size = 4949429, upload-time = "2025-06-28T18:47:20.046Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cc/f6be27a5c656a43a5344e064d9ae004d4dcb1d3c9d4f323c8189ddfe4d13/lxml-6.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58ffd35bd5425c3c3b9692d078bf7ab851441434531a7e517c4984d5634cd65b", size = 5087909, upload-time = "2025-06-28T18:47:22.834Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e6/8ec91b5bfbe6972458bc105aeb42088e50e4b23777170404aab5dfb0c62d/lxml-6.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f720a14aa102a38907c6d5030e3d66b3b680c3e6f6bc95473931ea3c00c59967", size = 5031713, upload-time = "2025-06-26T16:25:43.226Z" }, + { url = "https://files.pythonhosted.org/packages/33/cf/05e78e613840a40e5be3e40d892c48ad3e475804db23d4bad751b8cadb9b/lxml-6.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a5e8d207311a0170aca0eb6b160af91adc29ec121832e4ac151a57743a1e1e", size = 5232417, upload-time = "2025-06-26T16:25:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/ac/8c/6b306b3e35c59d5f0b32e3b9b6b3b0739b32c0dc42a295415ba111e76495/lxml-6.0.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:2dd1cc3ea7e60bfb31ff32cafe07e24839df573a5e7c2d33304082a5019bcd58", size = 4681443, upload-time = "2025-06-26T16:25:48.837Z" }, + { url = "https://files.pythonhosted.org/packages/59/43/0bd96bece5f7eea14b7220476835a60d2b27f8e9ca99c175f37c085cb154/lxml-6.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cfcf84f1defed7e5798ef4f88aa25fcc52d279be731ce904789aa7ccfb7e8d2", size = 5074542, upload-time = "2025-06-26T16:25:51.65Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3d/32103036287a8ca012d8518071f8852c68f2b3bfe048cef2a0202eb05910/lxml-6.0.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a52a4704811e2623b0324a18d41ad4b9fabf43ce5ff99b14e40a520e2190c851", size = 4729471, upload-time = "2025-06-26T16:25:54.571Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a8/7be5d17df12d637d81854bd8648cd329f29640a61e9a72a3f77add4a311b/lxml-6.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c16304bba98f48a28ae10e32a8e75c349dd742c45156f297e16eeb1ba9287a1f", size = 5256285, upload-time = "2025-06-26T16:25:56.997Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d0/6cb96174c25e0d749932557c8d51d60c6e292c877b46fae616afa23ed31a/lxml-6.0.0-cp311-cp311-win32.whl", hash = "sha256:f8d19565ae3eb956d84da3ef367aa7def14a2735d05bd275cd54c0301f0d0d6c", size = 3612004, upload-time = "2025-06-26T16:25:59.11Z" }, + { url = "https://files.pythonhosted.org/packages/ca/77/6ad43b165dfc6dead001410adeb45e88597b25185f4479b7ca3b16a5808f/lxml-6.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b2d71cdefda9424adff9a3607ba5bbfc60ee972d73c21c7e3c19e71037574816", size = 4003470, upload-time = "2025-06-26T16:26:01.655Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bc/4c50ec0eb14f932a18efc34fc86ee936a66c0eb5f2fe065744a2da8a68b2/lxml-6.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:8a2e76efbf8772add72d002d67a4c3d0958638696f541734304c7f28217a9cab", size = 3682477, upload-time = "2025-06-26T16:26:03.808Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/d01d735c298d7e0ddcedf6f028bf556577e5ab4f4da45175ecd909c79378/lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108", size = 8429515, upload-time = "2025-06-26T16:26:06.776Z" }, + { url = "https://files.pythonhosted.org/packages/06/37/0e3eae3043d366b73da55a86274a590bae76dc45aa004b7042e6f97803b1/lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be", size = 4601387, upload-time = "2025-06-26T16:26:09.511Z" }, + { url = "https://files.pythonhosted.org/packages/a3/28/e1a9a881e6d6e29dda13d633885d13acb0058f65e95da67841c8dd02b4a8/lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab", size = 5228928, upload-time = "2025-06-26T16:26:12.337Z" }, + { url = "https://files.pythonhosted.org/packages/9a/55/2cb24ea48aa30c99f805921c1c7860c1f45c0e811e44ee4e6a155668de06/lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563", size = 4952289, upload-time = "2025-06-28T18:47:25.602Z" }, + { url = "https://files.pythonhosted.org/packages/31/c0/b25d9528df296b9a3306ba21ff982fc5b698c45ab78b94d18c2d6ae71fd9/lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7", size = 5111310, upload-time = "2025-06-28T18:47:28.136Z" }, + { url = "https://files.pythonhosted.org/packages/e9/af/681a8b3e4f668bea6e6514cbcb297beb6de2b641e70f09d3d78655f4f44c/lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7", size = 5025457, upload-time = "2025-06-26T16:26:15.068Z" }, + { url = "https://files.pythonhosted.org/packages/99/b6/3a7971aa05b7be7dfebc7ab57262ec527775c2c3c5b2f43675cac0458cad/lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991", size = 5657016, upload-time = "2025-07-03T19:19:06.008Z" }, + { url = "https://files.pythonhosted.org/packages/69/f8/693b1a10a891197143c0673fcce5b75fc69132afa81a36e4568c12c8faba/lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da", size = 5257565, upload-time = "2025-06-26T16:26:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/96/e08ff98f2c6426c98c8964513c5dab8d6eb81dadcd0af6f0c538ada78d33/lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e", size = 4713390, upload-time = "2025-06-26T16:26:20.292Z" }, + { url = "https://files.pythonhosted.org/packages/a8/83/6184aba6cc94d7413959f6f8f54807dc318fdcd4985c347fe3ea6937f772/lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741", size = 5066103, upload-time = "2025-06-26T16:26:22.765Z" }, + { url = "https://files.pythonhosted.org/packages/ee/01/8bf1f4035852d0ff2e36a4d9aacdbcc57e93a6cd35a54e05fa984cdf73ab/lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3", size = 4791428, upload-time = "2025-06-26T16:26:26.461Z" }, + { url = "https://files.pythonhosted.org/packages/29/31/c0267d03b16954a85ed6b065116b621d37f559553d9339c7dcc4943a76f1/lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16", size = 5678523, upload-time = "2025-07-03T19:19:09.837Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f7/5495829a864bc5f8b0798d2b52a807c89966523140f3d6fa3a58ab6720ea/lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0", size = 5281290, upload-time = "2025-06-26T16:26:29.406Z" }, + { url = "https://files.pythonhosted.org/packages/79/56/6b8edb79d9ed294ccc4e881f4db1023af56ba451909b9ce79f2a2cd7c532/lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a", size = 3613495, upload-time = "2025-06-26T16:26:31.588Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1e/cc32034b40ad6af80b6fd9b66301fc0f180f300002e5c3eb5a6110a93317/lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3", size = 4014711, upload-time = "2025-06-26T16:26:33.723Z" }, + { url = "https://files.pythonhosted.org/packages/55/10/dc8e5290ae4c94bdc1a4c55865be7e1f31dfd857a88b21cbba68b5fea61b/lxml-6.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:8cb26f51c82d77483cdcd2b4a53cda55bbee29b3c2f3ddeb47182a2a9064e4eb", size = 3674431, upload-time = "2025-06-26T16:26:35.959Z" }, ] [[package]] @@ -895,7 +838,7 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.10.3" +version = "3.10.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, @@ -908,20 +851,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]] @@ -1047,16 +995,16 @@ wheels = [ [[package]] name = "msal" -version = "1.32.3" +version = "1.33.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/90/81dcc50f0be11a8c4dcbae1a9f761a26e5f905231330a7cacc9f04ec4c61/msal-1.32.3.tar.gz", hash = "sha256:5eea038689c78a5a70ca8ecbe1245458b55a857bd096efb6989c69ba15985d35", size = 151449, upload-time = "2025-04-25T13:12:34.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/da/81acbe0c1fd7e9e4ec35f55dadeba9833a847b9a6ba2e2d1e4432da901dd/msal-1.33.0.tar.gz", hash = "sha256:836ad80faa3e25a7d71015c990ce61f704a87328b1e73bcbb0623a18cbf17510", size = 153801, upload-time = "2025-07-22T19:36:33.693Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/bf/81516b9aac7fd867709984d08eb4db1d2e3fe1df795c8e442cde9b568962/msal-1.32.3-py3-none-any.whl", hash = "sha256:b2798db57760b1961b142f027ffb7c8169536bf77316e99a0df5c4aaebb11569", size = 115358, upload-time = "2025-04-25T13:12:33.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/5b/fbc73e91f7727ae1e79b21ed833308e99dc11cc1cd3d4717f579775de5e9/msal-1.33.0-py3-none-any.whl", hash = "sha256:c0cd41cecf8eaed733ee7e3be9e040291eba53b0f262d3ae9c58f38b04244273", size = 116853, upload-time = "2025-07-22T19:36:32.403Z" }, ] [[package]] @@ -1073,73 +1021,73 @@ wheels = [ [[package]] name = "multidict" -version = "6.5.0" +version = "6.6.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/b5/59f27b4ce9951a4bce56b88ba5ff5159486797ab18863f2b4c1c5e8465bd/multidict-6.5.0.tar.gz", hash = "sha256:942bd8002492ba819426a8d7aefde3189c1b87099cdf18aaaefefcf7f3f7b6d2", size = 98512, upload-time = "2025-06-17T14:15:56.556Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/ba/484f8e96ee58ec4fef42650eb9dbbedb24f9bc155780888398a4725d2270/multidict-6.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8b4bf6bb15a05796a07a248084e3e46e032860c899c7a9b981030e61368dba95", size = 73283, upload-time = "2025-06-17T14:13:50.406Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/01d62ea6199d76934c87746695b3ed16aeedfdd564e8d89184577037baac/multidict-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46bb05d50219655c42a4b8fcda9c7ee658a09adbb719c48e65a20284e36328ea", size = 42937, upload-time = "2025-06-17T14:13:51.45Z" }, - { url = "https://files.pythonhosted.org/packages/da/cf/bb462d920f26d9e2e0aff8a78aeb06af1225b826e9a5468870c57591910a/multidict-6.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:54f524d73f4d54e87e03c98f6af601af4777e4668a52b1bd2ae0a4d6fc7b392b", size = 42748, upload-time = "2025-06-17T14:13:52.505Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b1/d5c11ea0fdad68d3ed45f0e2527de6496d2fac8afe6b8ca6d407c20ad00f/multidict-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529b03600466480ecc502000d62e54f185a884ed4570dee90d9a273ee80e37b5", size = 236448, upload-time = "2025-06-17T14:13:53.562Z" }, - { url = "https://files.pythonhosted.org/packages/fc/69/c3ceb264994f5b338c812911a8d660084f37779daef298fc30bd817f75c7/multidict-6.5.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69ad681ad7c93a41ee7005cc83a144b5b34a3838bcf7261e2b5356057b0f78de", size = 228695, upload-time = "2025-06-17T14:13:54.775Z" }, - { url = "https://files.pythonhosted.org/packages/81/3d/c23dcc0d34a35ad29974184db2878021d28fe170ecb9192be6bfee73f1f2/multidict-6.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fe9fada8bc0839466b09fa3f6894f003137942984843ec0c3848846329a36ae", size = 247434, upload-time = "2025-06-17T14:13:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/06/b3/06cf7a049129ff52525a859277abb5648e61d7afae7fb7ed02e3806be34e/multidict-6.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f94c6ea6405fcf81baef1e459b209a78cda5442e61b5b7a57ede39d99b5204a0", size = 239431, upload-time = "2025-06-17T14:13:57.33Z" }, - { url = "https://files.pythonhosted.org/packages/8a/72/b2fe2fafa23af0c6123aebe23b4cd23fdad01dfe7009bb85624e4636d0dd/multidict-6.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ca75ad8a39ed75f079a8931435a5b51ee4c45d9b32e1740f99969a5d1cc2ee", size = 231542, upload-time = "2025-06-17T14:13:58.597Z" }, - { url = "https://files.pythonhosted.org/packages/a1/c9/a52ca0a342a02411a31b6af197a6428a5137d805293f10946eeab614ec06/multidict-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4c08f3a2a6cc42b414496017928d95898964fed84b1b2dace0c9ee763061f9", size = 233069, upload-time = "2025-06-17T14:13:59.834Z" }, - { url = "https://files.pythonhosted.org/packages/9b/55/a3328a3929b8e131e2678d5e65f552b0a6874fab62123e31f5a5625650b0/multidict-6.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:046a7540cfbb4d5dc846a1fd9843f3ba980c6523f2e0c5b8622b4a5c94138ae6", size = 250596, upload-time = "2025-06-17T14:14:01.178Z" }, - { url = "https://files.pythonhosted.org/packages/6c/b8/aa3905a38a8287013aeb0a54c73f79ccd8b32d2f1d53e5934643a36502c2/multidict-6.5.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64306121171d988af77d74be0d8c73ee1a69cf6f96aea7fa6030c88f32a152dd", size = 237858, upload-time = "2025-06-17T14:14:03.232Z" }, - { url = "https://files.pythonhosted.org/packages/d3/eb/f11d5af028014f402e5dd01ece74533964fa4e7bfae4af4824506fa8c398/multidict-6.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b4ac1dd5eb0ecf6f7351d5a9137f30a83f7182209c5d37f61614dfdce5714853", size = 249175, upload-time = "2025-06-17T14:14:04.561Z" }, - { url = "https://files.pythonhosted.org/packages/ac/57/d451905a62e5ef489cb4f92e8190d34ac5329427512afd7f893121da4e96/multidict-6.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bab4a8337235365f4111a7011a1f028826ca683834ebd12de4b85e2844359c36", size = 259532, upload-time = "2025-06-17T14:14:05.798Z" }, - { url = "https://files.pythonhosted.org/packages/d3/90/ff82b5ac5cabe3c79c50cf62a62f3837905aa717e67b6b4b7872804f23c8/multidict-6.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a05b5604c5a75df14a63eeeca598d11b2c3745b9008539b70826ea044063a572", size = 250554, upload-time = "2025-06-17T14:14:07.382Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5a/0cabc50d4bc16e61d8b0a8a74499a1409fa7b4ef32970b7662a423781fc7/multidict-6.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67c4a640952371c9ca65b6a710598be246ef3be5ca83ed38c16a7660d3980877", size = 248159, upload-time = "2025-06-17T14:14:08.65Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1d/adeabae0771544f140d9f42ab2c46eaf54e793325999c36106078b7f6600/multidict-6.5.0-cp311-cp311-win32.whl", hash = "sha256:fdeae096ca36c12d8aca2640b8407a9d94e961372c68435bef14e31cce726138", size = 40357, upload-time = "2025-06-17T14:14:09.91Z" }, - { url = "https://files.pythonhosted.org/packages/e1/fe/bbd85ae65c96de5c9910c332ee1f4b7be0bf0fb21563895167bcb6502a1f/multidict-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:e2977ef8b7ce27723ee8c610d1bd1765da4f3fbe5a64f9bf1fd3b4770e31fbc0", size = 44432, upload-time = "2025-06-17T14:14:11.013Z" }, - { url = "https://files.pythonhosted.org/packages/96/af/f9052d9c4e65195b210da9f7afdea06d3b7592b3221cc0ef1b407f762faa/multidict-6.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:82d0cf0ea49bae43d9e8c3851e21954eff716259ff42da401b668744d1760bcb", size = 41408, upload-time = "2025-06-17T14:14:12.112Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fa/18f4950e00924f7e84c8195f4fc303295e14df23f713d64e778b8fa8b903/multidict-6.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1bb986c8ea9d49947bc325c51eced1ada6d8d9b4c5b15fd3fcdc3c93edef5a74", size = 73474, upload-time = "2025-06-17T14:14:13.528Z" }, - { url = "https://files.pythonhosted.org/packages/6c/66/0392a2a8948bccff57e4793c9dde3e5c088f01e8b7f8867ee58a2f187fc5/multidict-6.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:03c0923da300120830fc467e23805d63bbb4e98b94032bd863bc7797ea5fa653", size = 43741, upload-time = "2025-06-17T14:14:15.188Z" }, - { url = "https://files.pythonhosted.org/packages/98/3e/f48487c91b2a070566cfbab876d7e1ebe7deb0a8002e4e896a97998ae066/multidict-6.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4c78d5ec00fdd35c91680ab5cf58368faad4bd1a8721f87127326270248de9bc", size = 42143, upload-time = "2025-06-17T14:14:16.612Z" }, - { url = "https://files.pythonhosted.org/packages/3f/49/439c6cc1cd00365cf561bdd3579cc3fa1a0d38effb3a59b8d9562839197f/multidict-6.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadc3cb78be90a887f8f6b73945b840da44b4a483d1c9750459ae69687940c97", size = 239303, upload-time = "2025-06-17T14:14:17.707Z" }, - { url = "https://files.pythonhosted.org/packages/c4/24/491786269e90081cb536e4d7429508725bc92ece176d1204a4449de7c41c/multidict-6.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b02e1ca495d71e07e652e4cef91adae3bf7ae4493507a263f56e617de65dafc", size = 236913, upload-time = "2025-06-17T14:14:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/e8/76/bbe2558b820ebeca8a317ab034541790e8160ca4b1e450415383ac69b339/multidict-6.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7fe92a62326eef351668eec4e2dfc494927764a0840a1895cff16707fceffcd3", size = 250752, upload-time = "2025-06-17T14:14:20.297Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e3/3977f2c1123f553ceff9f53cd4de04be2c1912333c6fabbcd51531655476/multidict-6.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7673ee4f63879ecd526488deb1989041abcb101b2d30a9165e1e90c489f3f7fb", size = 243937, upload-time = "2025-06-17T14:14:21.935Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b8/7a6e9c13c79709cdd2f22ee849f058e6da76892d141a67acc0e6c30d845c/multidict-6.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa097ae2a29f573de7e2d86620cbdda5676d27772d4ed2669cfa9961a0d73955", size = 237419, upload-time = "2025-06-17T14:14:23.215Z" }, - { url = "https://files.pythonhosted.org/packages/84/9d/8557f5e88da71bc7e7a8ace1ada4c28197f3bfdc2dd6e51d3b88f2e16e8e/multidict-6.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:300da0fa4f8457d9c4bd579695496116563409e676ac79b5e4dca18e49d1c308", size = 237222, upload-time = "2025-06-17T14:14:24.516Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3b/8f023ad60e7969cb6bc0683738d0e1618f5ff5723d6d2d7818dc6df6ad3d/multidict-6.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a19bd108c35877b57393243d392d024cfbfdefe759fd137abb98f6fc910b64c", size = 247861, upload-time = "2025-06-17T14:14:25.839Z" }, - { url = "https://files.pythonhosted.org/packages/af/1c/9cf5a099ce7e3189906cf5daa72c44ee962dcb4c1983659f3a6f8a7446ab/multidict-6.5.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f32a1777465a35c35ddbbd7fc1293077938a69402fcc59e40b2846d04a120dd", size = 243917, upload-time = "2025-06-17T14:14:27.164Z" }, - { url = "https://files.pythonhosted.org/packages/6c/bb/88ee66ebeef56868044bac58feb1cc25658bff27b20e3cfc464edc181287/multidict-6.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9cc1e10c14ce8112d1e6d8971fe3cdbe13e314f68bea0e727429249d4a6ce164", size = 249214, upload-time = "2025-06-17T14:14:28.795Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/a90e88cc4a1309f33088ab1cdd5c0487718f49dfb82c5ffc845bb17c1973/multidict-6.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e95c5e07a06594bdc288117ca90e89156aee8cb2d7c330b920d9c3dd19c05414", size = 258682, upload-time = "2025-06-17T14:14:30.066Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d8/16dd69a6811920a31f4e06114ebe67b1cd922c8b05c9c82b050706d0b6fe/multidict-6.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:40ff26f58323795f5cd2855e2718a1720a1123fb90df4553426f0efd76135462", size = 254254, upload-time = "2025-06-17T14:14:31.323Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a8/90193a5f5ca1bdbf92633d69a25a2ef9bcac7b412b8d48c84d01a2732518/multidict-6.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76803a29fd71869a8b59c2118c9dcfb3b8f9c8723e2cce6baeb20705459505cf", size = 247741, upload-time = "2025-06-17T14:14:32.717Z" }, - { url = "https://files.pythonhosted.org/packages/cd/43/29c7a747153c05b41d1f67455426af39ed88d6de3f21c232b8f2724bde13/multidict-6.5.0-cp312-cp312-win32.whl", hash = "sha256:df7ecbc65a53a2ce1b3a0c82e6ad1a43dcfe7c6137733f9176a92516b9f5b851", size = 41049, upload-time = "2025-06-17T14:14:33.941Z" }, - { url = "https://files.pythonhosted.org/packages/1e/e8/8f3fc32b7e901f3a2719764d64aeaf6ae77b4ba961f1c3a3cf3867766636/multidict-6.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ec1c3fbbb0b655a6540bce408f48b9a7474fd94ed657dcd2e890671fefa7743", size = 44700, upload-time = "2025-06-17T14:14:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/24/e4/e250806adc98d524d41e69c8d4a42bc3513464adb88cb96224df12928617/multidict-6.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:2d24a00d34808b22c1f15902899b9d82d0faeca9f56281641c791d8605eacd35", size = 41703, upload-time = "2025-06-17T14:14:36.168Z" }, - { url = "https://files.pythonhosted.org/packages/44/d8/45e8fc9892a7386d074941429e033adb4640e59ff0780d96a8cf46fe788e/multidict-6.5.0-py3-none-any.whl", hash = "sha256:5634b35f225977605385f56153bd95a7133faffc0ffe12ad26e10517537e8dfc", size = 12181, upload-time = "2025-06-17T14:15:55.156Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7f/90a7f01e2d005d6653c689039977f6856718c75c5579445effb7e60923d1/multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c", size = 76472, upload-time = "2025-08-11T12:06:29.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a3/bed07bc9e2bb302ce752f1dabc69e884cd6a676da44fb0e501b246031fdd/multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb", size = 44634, upload-time = "2025-08-11T12:06:30.374Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4b/ceeb4f8f33cf81277da464307afeaf164fb0297947642585884f5cad4f28/multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e", size = 44282, upload-time = "2025-08-11T12:06:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/436a5da8702b06866189b69f655ffdb8f70796252a8772a77815f1812679/multidict-6.6.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded", size = 229696, upload-time = "2025-08-11T12:06:33.087Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0e/915160be8fecf1fca35f790c08fb74ca684d752fcba62c11daaf3d92c216/multidict-6.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683", size = 246665, upload-time = "2025-08-11T12:06:34.448Z" }, + { url = "https://files.pythonhosted.org/packages/08/ee/2f464330acd83f77dcc346f0b1a0eaae10230291450887f96b204b8ac4d3/multidict-6.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a", size = 225485, upload-time = "2025-08-11T12:06:35.672Z" }, + { url = "https://files.pythonhosted.org/packages/71/cc/9a117f828b4d7fbaec6adeed2204f211e9caf0a012692a1ee32169f846ae/multidict-6.6.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9", size = 257318, upload-time = "2025-08-11T12:06:36.98Z" }, + { url = "https://files.pythonhosted.org/packages/25/77/62752d3dbd70e27fdd68e86626c1ae6bccfebe2bb1f84ae226363e112f5a/multidict-6.6.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50", size = 254689, upload-time = "2025-08-11T12:06:38.233Z" }, + { url = "https://files.pythonhosted.org/packages/00/6e/fac58b1072a6fc59af5e7acb245e8754d3e1f97f4f808a6559951f72a0d4/multidict-6.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52", size = 246709, upload-time = "2025-08-11T12:06:39.517Z" }, + { url = "https://files.pythonhosted.org/packages/01/ef/4698d6842ef5e797c6db7744b0081e36fb5de3d00002cc4c58071097fac3/multidict-6.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6", size = 243185, upload-time = "2025-08-11T12:06:40.796Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c9/d82e95ae1d6e4ef396934e9b0e942dfc428775f9554acf04393cce66b157/multidict-6.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e", size = 237838, upload-time = "2025-08-11T12:06:42.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/f94af5c36baaa75d44fab9f02e2a6bcfa0cd90acb44d4976a80960759dbc/multidict-6.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3", size = 246368, upload-time = "2025-08-11T12:06:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/4a/fe/29f23460c3d995f6a4b678cb2e9730e7277231b981f0b234702f0177818a/multidict-6.6.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c", size = 253339, upload-time = "2025-08-11T12:06:45.597Z" }, + { url = "https://files.pythonhosted.org/packages/29/b6/fd59449204426187b82bf8a75f629310f68c6adc9559dc922d5abe34797b/multidict-6.6.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b", size = 246933, upload-time = "2025-08-11T12:06:46.841Z" }, + { url = "https://files.pythonhosted.org/packages/19/52/d5d6b344f176a5ac3606f7a61fb44dc746e04550e1a13834dff722b8d7d6/multidict-6.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f", size = 242225, upload-time = "2025-08-11T12:06:48.588Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d3/5b2281ed89ff4d5318d82478a2a2450fcdfc3300da48ff15c1778280ad26/multidict-6.6.4-cp311-cp311-win32.whl", hash = "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2", size = 41306, upload-time = "2025-08-11T12:06:49.95Z" }, + { url = "https://files.pythonhosted.org/packages/74/7d/36b045c23a1ab98507aefd44fd8b264ee1dd5e5010543c6fccf82141ccef/multidict-6.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e", size = 46029, upload-time = "2025-08-11T12:06:51.082Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5e/553d67d24432c5cd52b49047f2d248821843743ee6d29a704594f656d182/multidict-6.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf", size = 43017, upload-time = "2025-08-11T12:06:52.243Z" }, + { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516, upload-time = "2025-08-11T12:06:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394, upload-time = "2025-08-11T12:06:54.555Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591, upload-time = "2025-08-11T12:06:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215, upload-time = "2025-08-11T12:06:57.213Z" }, + { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload-time = "2025-08-11T12:06:58.946Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload-time = "2025-08-11T12:07:00.301Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload-time = "2025-08-11T12:07:01.638Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload-time = "2025-08-11T12:07:02.943Z" }, + { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload-time = "2025-08-11T12:07:04.564Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload-time = "2025-08-11T12:07:05.914Z" }, + { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload-time = "2025-08-11T12:07:08.301Z" }, + { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477, upload-time = "2025-08-11T12:07:10.248Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload-time = "2025-08-11T12:07:11.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload-time = "2025-08-11T12:07:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload-time = "2025-08-11T12:07:14.57Z" }, + { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888, upload-time = "2025-08-11T12:07:15.904Z" }, + { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072, upload-time = "2025-08-11T12:07:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222, upload-time = "2025-08-11T12:07:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, ] [[package]] name = "mypy" -version = "1.16.1" +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/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" } +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/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" }, - { url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" }, - { url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" }, - { url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" }, - { url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" }, - { url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" }, - { url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" }, - { url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" }, - { url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" }, - { url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" }, + { 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]] @@ -1162,30 +1110,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]] @@ -1245,7 +1202,6 @@ dependencies = [ { name = "inputs" }, { name = "json-rpc" }, { name = "libusb1" }, - { name = "llvmlite" }, { name = "numpy" }, { name = "onnx" }, { name = "psutil" }, @@ -1300,13 +1256,11 @@ docs = [ ] testing = [ { name = "codespell" }, - { name = "coverage" }, { name = "hypothesis" }, { name = "mypy" }, { name = "pre-commit-hooks" }, { name = "pytest" }, { name = "pytest-asyncio" }, - { name = "pytest-cov" }, { name = "pytest-cpp" }, { name = "pytest-mock" }, { name = "pytest-randomly" }, @@ -1318,7 +1272,6 @@ testing = [ ] tools = [ { name = "metadrive-simulator", marker = "platform_machine != 'aarch64'" }, - { name = "rerun-sdk" }, ] [package.metadata] @@ -1331,7 +1284,6 @@ requires-dist = [ { name = "casadi", specifier = ">=3.6.6" }, { name = "cffi" }, { name = "codespell", marker = "extra == 'testing'" }, - { name = "coverage", marker = "extra == 'testing'" }, { name = "crcmod" }, { name = "cython" }, { name = "dbus-next", marker = "extra == 'dev'" }, @@ -1342,13 +1294,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" }, @@ -1366,21 +1317,19 @@ requires-dist = [ { name = "pyserial" }, { name = "pytest", marker = "extra == 'testing'" }, { name = "pytest-asyncio", marker = "extra == 'testing'" }, - { name = "pytest-cov", marker = "extra == 'testing'" }, { name = "pytest-cpp", marker = "extra == 'testing'" }, { name = "pytest-mock", marker = "extra == 'testing'" }, { name = "pytest-randomly", marker = "extra == 'testing'" }, { 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" }, @@ -1473,39 +1422,39 @@ wheels = [ [[package]] name = "pillow" -version = "11.2.1" +version = "11.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/08/3fbf4b98924c73037a8e8b4c2c774784805e0fb4ebca6c5bb60795c40125/pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", size = 3198450, upload-time = "2025-04-12T17:47:37.135Z" }, - { url = "https://files.pythonhosted.org/packages/84/92/6505b1af3d2849d5e714fc75ba9e69b7255c05ee42383a35a4d58f576b16/pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", size = 3030550, upload-time = "2025-04-12T17:47:39.345Z" }, - { url = "https://files.pythonhosted.org/packages/3c/8c/ac2f99d2a70ff966bc7eb13dacacfaab57c0549b2ffb351b6537c7840b12/pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", size = 4415018, upload-time = "2025-04-12T17:47:41.128Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e3/0a58b5d838687f40891fff9cbaf8669f90c96b64dc8f91f87894413856c6/pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", size = 4498006, upload-time = "2025-04-12T17:47:42.912Z" }, - { url = "https://files.pythonhosted.org/packages/21/f5/6ba14718135f08fbfa33308efe027dd02b781d3f1d5c471444a395933aac/pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", size = 4517773, upload-time = "2025-04-12T17:47:44.611Z" }, - { url = "https://files.pythonhosted.org/packages/20/f2/805ad600fc59ebe4f1ba6129cd3a75fb0da126975c8579b8f57abeb61e80/pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", size = 4607069, upload-time = "2025-04-12T17:47:46.46Z" }, - { url = "https://files.pythonhosted.org/packages/71/6b/4ef8a288b4bb2e0180cba13ca0a519fa27aa982875882392b65131401099/pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", size = 4583460, upload-time = "2025-04-12T17:47:49.255Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/f29c705a09cbc9e2a456590816e5c234382ae5d32584f451c3eb41a62062/pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", size = 4661304, upload-time = "2025-04-12T17:47:51.067Z" }, - { url = "https://files.pythonhosted.org/packages/6e/1a/c8217b6f2f73794a5e219fbad087701f412337ae6dbb956db37d69a9bc43/pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", size = 2331809, upload-time = "2025-04-12T17:47:54.425Z" }, - { url = "https://files.pythonhosted.org/packages/e2/72/25a8f40170dc262e86e90f37cb72cb3de5e307f75bf4b02535a61afcd519/pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", size = 2676338, upload-time = "2025-04-12T17:47:56.535Z" }, - { url = "https://files.pythonhosted.org/packages/06/9e/76825e39efee61efea258b479391ca77d64dbd9e5804e4ad0fa453b4ba55/pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", size = 2414918, upload-time = "2025-04-12T17:47:58.217Z" }, - { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185, upload-time = "2025-04-12T17:48:00.417Z" }, - { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306, upload-time = "2025-04-12T17:48:02.391Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121, upload-time = "2025-04-12T17:48:04.554Z" }, - { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707, upload-time = "2025-04-12T17:48:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921, upload-time = "2025-04-12T17:48:09.229Z" }, - { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523, upload-time = "2025-04-12T17:48:11.631Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836, upload-time = "2025-04-12T17:48:13.592Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390, upload-time = "2025-04-12T17:48:15.938Z" }, - { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309, upload-time = "2025-04-12T17:48:17.885Z" }, - { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768, upload-time = "2025-04-12T17:48:19.655Z" }, - { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ad/2613c04633c7257d9481ab21d6b5364b59fc5d75faafd7cb8693523945a3/pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", size = 3181734, upload-time = "2025-04-12T17:49:46.789Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fd/dcdda4471ed667de57bb5405bb42d751e6cfdd4011a12c248b455c778e03/pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", size = 2999841, upload-time = "2025-04-12T17:49:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/ac/89/8a2536e95e77432833f0db6fd72a8d310c8e4272a04461fb833eb021bf94/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", size = 3437470, upload-time = "2025-04-12T17:49:50.831Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8f/abd47b73c60712f88e9eda32baced7bfc3e9bd6a7619bb64b93acff28c3e/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", size = 3460013, upload-time = "2025-04-12T17:49:53.278Z" }, - { url = "https://files.pythonhosted.org/packages/f6/20/5c0a0aa83b213b7a07ec01e71a3d6ea2cf4ad1d2c686cc0168173b6089e7/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", size = 3527165, upload-time = "2025-04-12T17:49:55.164Z" }, - { url = "https://files.pythonhosted.org/packages/58/0e/2abab98a72202d91146abc839e10c14f7cf36166f12838ea0c4db3ca6ecb/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", size = 3571586, upload-time = "2025-04-12T17:49:57.171Z" }, - { url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751, upload-time = "2025-04-12T17:49:59.628Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, ] [[package]] @@ -1528,14 +1477,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]] @@ -1614,32 +1563,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 = "20.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/ee/a7810cb9f3d6e9238e61d312076a9859bf3668fd21c69744de9532383912/pyarrow-20.0.0.tar.gz", hash = "sha256:febc4a913592573c8d5805091a6c2b5064c8bd6e002131f01061797d91c783c1", size = 1125187, upload-time = "2025-04-27T12:34:23.264Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/a2/b7930824181ceadd0c63c1042d01fa4ef63eee233934826a7a2a9af6e463/pyarrow-20.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:24ca380585444cb2a31324c546a9a56abbe87e26069189e14bdba19c86c049f0", size = 30856035, upload-time = "2025-04-27T12:28:40.78Z" }, - { url = "https://files.pythonhosted.org/packages/9b/18/c765770227d7f5bdfa8a69f64b49194352325c66a5c3bb5e332dfd5867d9/pyarrow-20.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:95b330059ddfdc591a3225f2d272123be26c8fa76e8c9ee1a77aad507361cfdb", size = 32309552, upload-time = "2025-04-27T12:28:47.051Z" }, - { url = "https://files.pythonhosted.org/packages/44/fb/dfb2dfdd3e488bb14f822d7335653092dde150cffc2da97de6e7500681f9/pyarrow-20.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f0fb1041267e9968c6d0d2ce3ff92e3928b243e2b6d11eeb84d9ac547308232", size = 41334704, upload-time = "2025-04-27T12:28:55.064Z" }, - { url = "https://files.pythonhosted.org/packages/58/0d/08a95878d38808051a953e887332d4a76bc06c6ee04351918ee1155407eb/pyarrow-20.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8ff87cc837601532cc8242d2f7e09b4e02404de1b797aee747dd4ba4bd6313f", size = 42399836, upload-time = "2025-04-27T12:29:02.13Z" }, - { url = "https://files.pythonhosted.org/packages/f3/cd/efa271234dfe38f0271561086eedcad7bc0f2ddd1efba423916ff0883684/pyarrow-20.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:7a3a5dcf54286e6141d5114522cf31dd67a9e7c9133d150799f30ee302a7a1ab", size = 40711789, upload-time = "2025-04-27T12:29:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/46/1f/7f02009bc7fc8955c391defee5348f510e589a020e4b40ca05edcb847854/pyarrow-20.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a6ad3e7758ecf559900261a4df985662df54fb7fdb55e8e3b3aa99b23d526b62", size = 42301124, upload-time = "2025-04-27T12:29:17.187Z" }, - { url = "https://files.pythonhosted.org/packages/4f/92/692c562be4504c262089e86757a9048739fe1acb4024f92d39615e7bab3f/pyarrow-20.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6bb830757103a6cb300a04610e08d9636f0cd223d32f388418ea893a3e655f1c", size = 42916060, upload-time = "2025-04-27T12:29:24.253Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ec/9f5c7e7c828d8e0a3c7ef50ee62eca38a7de2fa6eb1b8fa43685c9414fef/pyarrow-20.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96e37f0766ecb4514a899d9a3554fadda770fb57ddf42b63d80f14bc20aa7db3", size = 44547640, upload-time = "2025-04-27T12:29:32.782Z" }, - { url = "https://files.pythonhosted.org/packages/54/96/46613131b4727f10fd2ffa6d0d6f02efcc09a0e7374eff3b5771548aa95b/pyarrow-20.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3346babb516f4b6fd790da99b98bed9708e3f02e734c84971faccb20736848dc", size = 25781491, upload-time = "2025-04-27T12:29:38.464Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d6/0c10e0d54f6c13eb464ee9b67a68b8c71bcf2f67760ef5b6fbcddd2ab05f/pyarrow-20.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:75a51a5b0eef32727a247707d4755322cb970be7e935172b6a3a9f9ae98404ba", size = 30815067, upload-time = "2025-04-27T12:29:44.384Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e2/04e9874abe4094a06fd8b0cbb0f1312d8dd7d707f144c2ec1e5e8f452ffa/pyarrow-20.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:211d5e84cecc640c7a3ab900f930aaff5cd2702177e0d562d426fb7c4f737781", size = 32297128, upload-time = "2025-04-27T12:29:52.038Z" }, - { url = "https://files.pythonhosted.org/packages/31/fd/c565e5dcc906a3b471a83273039cb75cb79aad4a2d4a12f76cc5ae90a4b8/pyarrow-20.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ba3cf4182828be7a896cbd232aa8dd6a31bd1f9e32776cc3796c012855e1199", size = 41334890, upload-time = "2025-04-27T12:29:59.452Z" }, - { url = "https://files.pythonhosted.org/packages/af/a9/3bdd799e2c9b20c1ea6dc6fa8e83f29480a97711cf806e823f808c2316ac/pyarrow-20.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c3a01f313ffe27ac4126f4c2e5ea0f36a5fc6ab51f8726cf41fee4b256680bd", size = 42421775, upload-time = "2025-04-27T12:30:06.875Z" }, - { url = "https://files.pythonhosted.org/packages/10/f7/da98ccd86354c332f593218101ae56568d5dcedb460e342000bd89c49cc1/pyarrow-20.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a2791f69ad72addd33510fec7bb14ee06c2a448e06b649e264c094c5b5f7ce28", size = 40687231, upload-time = "2025-04-27T12:30:13.954Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1b/2168d6050e52ff1e6cefc61d600723870bf569cbf41d13db939c8cf97a16/pyarrow-20.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4250e28a22302ce8692d3a0e8ec9d9dde54ec00d237cff4dfa9c1fbf79e472a8", size = 42295639, upload-time = "2025-04-27T12:30:21.949Z" }, - { url = "https://files.pythonhosted.org/packages/b2/66/2d976c0c7158fd25591c8ca55aee026e6d5745a021915a1835578707feb3/pyarrow-20.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:89e030dc58fc760e4010148e6ff164d2f44441490280ef1e97a542375e41058e", size = 42908549, upload-time = "2025-04-27T12:30:29.551Z" }, - { url = "https://files.pythonhosted.org/packages/31/a9/dfb999c2fc6911201dcbf348247f9cc382a8990f9ab45c12eabfd7243a38/pyarrow-20.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6102b4864d77102dbbb72965618e204e550135a940c2534711d5ffa787df2a5a", size = 44557216, upload-time = "2025-04-27T12:30:36.977Z" }, - { url = "https://files.pythonhosted.org/packages/a0/8e/9adee63dfa3911be2382fb4d92e4b2e7d82610f9d9f668493bebaa2af50f/pyarrow-20.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:96d6a0a37d9c98be08f5ed6a10831d88d52cac7b13f5287f1e0f625a0de8062b", size = 25660496, upload-time = "2025-04-27T12:30:42.809Z" }, -] - [[package]] name = "pyaudio" version = "0.2.14" @@ -1845,164 +1768,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 = [ @@ -2024,9 +1947,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 = [ @@ -2039,8 +1962,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 = [ @@ -2052,8 +1975,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 = [ @@ -2066,8 +1989,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 = [ @@ -2079,8 +2002,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 = [ @@ -2092,8 +2015,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 = [ @@ -2105,8 +2028,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 = [ @@ -2118,10 +2041,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 = [ @@ -2134,8 +2057,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 = [ @@ -2147,8 +2070,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 = [ @@ -2161,8 +2084,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 = [ @@ -2175,8 +2098,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 = [ @@ -2189,8 +2112,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 = [ @@ -2203,11 +2126,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 = [ @@ -2220,9 +2143,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 = [ @@ -2235,8 +2158,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 = [ @@ -2249,8 +2172,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 = [ @@ -2263,11 +2186,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 = [ @@ -2280,8 +2203,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 = [ @@ -2293,8 +2216,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 = [ @@ -2306,8 +2229,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 = [ @@ -2320,8 +2243,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 = [ @@ -2333,8 +2256,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 = [ @@ -2347,11 +2270,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 = [ @@ -2363,8 +2286,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 = [ @@ -2377,11 +2300,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 = [ @@ -2393,7 +2316,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 = [ @@ -2406,8 +2329,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 = [ @@ -2419,8 +2342,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 = [ @@ -2432,8 +2355,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 = [ @@ -2446,9 +2369,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 = [ @@ -2461,8 +2384,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 = [ @@ -2475,9 +2398,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 = [ @@ -2490,8 +2413,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 = [ @@ -2504,8 +2427,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 = [ @@ -2518,8 +2441,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 = [ @@ -2531,8 +2454,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 = [ @@ -2545,8 +2468,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 = [ @@ -2559,8 +2482,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 = [ @@ -2573,8 +2496,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 = [ @@ -2587,8 +2510,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 = [ @@ -2601,8 +2524,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 = [ @@ -2615,9 +2538,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 = [ @@ -2630,8 +2553,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 = [ @@ -2644,9 +2567,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 = [ @@ -2659,8 +2582,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 = [ @@ -2673,8 +2596,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 = [ @@ -2687,8 +2610,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 = [ @@ -2700,8 +2623,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 = [ @@ -2713,8 +2636,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 = [ @@ -2726,8 +2649,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 = [ @@ -2739,8 +2662,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 = [ @@ -2753,9 +2676,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 = [ @@ -2767,8 +2690,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 = [ @@ -2780,8 +2703,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 = [ @@ -2793,8 +2716,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 = [ @@ -2806,8 +2729,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 = [ @@ -2819,8 +2742,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 = [ @@ -2832,8 +2755,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 = [ @@ -2846,8 +2769,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 = [ @@ -2860,8 +2783,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 = [ @@ -2874,8 +2797,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 = [ @@ -2887,8 +2810,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 = [ @@ -2900,8 +2823,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 = [ @@ -2914,8 +2837,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 = [ @@ -2928,8 +2851,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 = [ @@ -2942,8 +2865,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 = [ @@ -2956,9 +2879,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 = [ @@ -2971,9 +2894,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 = [ @@ -2986,8 +2909,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 = [ @@ -3000,8 +2923,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 = [ @@ -3014,8 +2937,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 = [ @@ -3028,8 +2951,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 = [ @@ -3041,9 +2964,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 = [ @@ -3055,8 +2978,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 = [ @@ -3069,8 +2992,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 = [ @@ -3083,8 +3006,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 = [ @@ -3097,8 +3020,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 = [ @@ -3110,8 +3033,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 = [ @@ -3123,8 +3046,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 = [ @@ -3136,8 +3059,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 = [ @@ -3149,8 +3072,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 = [ @@ -3162,8 +3085,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 = [ @@ -3175,8 +3098,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 = [ @@ -3189,8 +3112,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 = [ @@ -3203,9 +3126,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 = [ @@ -3217,9 +3140,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 = [ @@ -3232,9 +3155,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 = [ @@ -3246,8 +3169,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 = [ @@ -3259,10 +3182,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 = [ @@ -3275,8 +3198,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 = [ @@ -3288,10 +3211,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 = [ @@ -3304,9 +3227,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 = [ @@ -3318,8 +3241,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 = [ @@ -3331,8 +3254,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 = [ @@ -3345,8 +3268,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 = [ @@ -3359,8 +3282,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 = [ @@ -3373,9 +3296,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 = [ @@ -3388,8 +3311,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 = [ @@ -3402,8 +3325,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 = [ @@ -3415,8 +3338,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 = [ @@ -3429,8 +3352,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 = [ @@ -3442,9 +3365,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 = [ @@ -3457,8 +3380,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 = [ @@ -3471,8 +3394,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 = [ @@ -3484,8 +3407,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 = [ @@ -3497,8 +3420,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 = [ @@ -3511,8 +3434,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 = [ @@ -3525,8 +3448,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 = [ @@ -3539,8 +3462,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 = [ @@ -3552,8 +3475,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 = [ @@ -3565,10 +3488,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 = [ @@ -3581,8 +3504,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 = [ @@ -3595,8 +3518,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 = [ @@ -3608,8 +3531,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 = [ @@ -3621,8 +3544,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 = [ @@ -3635,8 +3558,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 = [ @@ -3649,8 +3572,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 = [ @@ -3662,8 +3585,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 = [ @@ -3676,8 +3599,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 = [ @@ -3690,9 +3613,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 = [ @@ -3704,8 +3627,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 = [ @@ -3718,8 +3641,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 = [ @@ -3732,9 +3655,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 = [ @@ -3747,9 +3670,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 = [ @@ -3762,9 +3685,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 = [ @@ -3777,8 +3700,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 = [ @@ -3791,8 +3714,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 = [ @@ -3804,8 +3727,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 = [ @@ -3818,8 +3741,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 = [ @@ -3831,8 +3754,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 = [ @@ -3845,9 +3768,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 = [ @@ -3859,9 +3782,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 = [ @@ -3874,9 +3797,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 = [ @@ -3888,9 +3811,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 = [ @@ -3902,8 +3825,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 = [ @@ -3915,8 +3838,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 = [ @@ -3929,8 +3852,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 = [ @@ -3943,8 +3866,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 = [ @@ -3957,8 +3880,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 = [ @@ -3970,8 +3893,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 = [ @@ -3983,8 +3906,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 = [ @@ -3997,9 +3920,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 = [ @@ -4012,8 +3935,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 = [ @@ -4026,8 +3949,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 = [ @@ -4039,9 +3962,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 = [ @@ -4054,8 +3977,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 = [ @@ -4068,8 +3991,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 = [ @@ -4082,8 +4005,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 = [ @@ -4095,8 +4018,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 = [ @@ -4108,8 +4031,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 = [ @@ -4122,9 +4045,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 = [ @@ -4136,8 +4059,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 = [ @@ -4149,10 +4072,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 = [ @@ -4165,8 +4088,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 = [ @@ -4179,10 +4102,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 = [ @@ -4195,8 +4118,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 = [ @@ -4302,28 +4225,14 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "1.0.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" }, -] - -[[package]] -name = "pytest-cov" -version = "6.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pluggy" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" }, ] [[package]] @@ -4401,8 +4310,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" }, @@ -4425,7 +4334,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 = [ @@ -4460,15 +4369,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/79/0c/c16bc93ac2755bac0 [[package]] name = "pywin32" -version = "310" +version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284, upload-time = "2025-03-17T00:55:53.124Z" }, - { url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748, upload-time = "2025-03-17T00:55:55.203Z" }, - { url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941, upload-time = "2025-03-17T00:55:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ec/4fdbe47932f671d6e348474ea35ed94227fb5df56a7c30cbbb42cd396ed0/pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d", size = 8796239, upload-time = "2025-03-17T00:55:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e5/b0627f8bb84e06991bea89ad8153a9e50ace40b2e1195d68e9dff6b03d0f/pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060", size = 9503839, upload-time = "2025-03-17T00:56:00.8Z" }, - { url = "https://files.pythonhosted.org/packages/1f/32/9ccf53748df72301a89713936645a664ec001abd35ecc8578beda593d37d/pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966", size = 8459470, upload-time = "2025-03-17T00:56:02.601Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, ] [[package]] @@ -4543,38 +4452,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]] @@ -4628,25 +4537,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.23.3" -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/00/54/710685ea836d91f14422de515262a9db5c2a17777e59ae4c1c19b46c1fa0/rerun_sdk-0.23.3-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8a048ace69443aeb4040046d4bc28ba17be2c6862df7c2161e21b0608d5d6e0f", size = 59486089, upload-time = "2025-05-26T16:08:08.395Z" }, - { url = "https://files.pythonhosted.org/packages/26/81/b94c23cde1b9ed54900f47e337f8e156d9fdec7494eaa11bbb592c26a5d9/rerun_sdk-0.23.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a3ef05acfa9315d956c69eb680a50e46c802ba27eb8025bab71e14a989299383", size = 55128863, upload-time = "2025-05-26T16:08:13.328Z" }, - { url = "https://files.pythonhosted.org/packages/02/bc/c32f71969a5d981087fded194012ef162391e49dba71ba5115a17c642065/rerun_sdk-0.23.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:0cc9c60f1180edca1a4d70dbe591eb108e4dbe599c31136949b8d4106410cd14", size = 61603766, upload-time = "2025-05-26T16:08:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ef/93b6a8932747842e5f7c27f3ce7ef82bbc046aad3b7d5b2e905ffc5614a7/rerun_sdk-0.23.3-cp39-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:49833bf3a4854eaf287a8bfe7dfa4cd8f67b0fd7917a469c8bb7061895fb1cf8", size = 64875203, upload-time = "2025-05-26T16:08:21.95Z" }, - { url = "https://files.pythonhosted.org/packages/c5/36/759287d69301a3ada86ee102369a9521dfef0bb91b215e98a6a7b262a311/rerun_sdk-0.23.3-cp39-abi3-win_amd64.whl", hash = "sha256:88639632116b3761e2a45f4e5ac39eaf3406b615f516baccb508d93c6b42cc4e", size = 50862558, upload-time = "2025-05-26T16:08:26.98Z" }, -] - [[package]] name = "ruamel-yaml" version = "0.18.14" @@ -4687,36 +4577,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.0" +version = "0.12.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" } +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/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" }, - { url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" }, - { url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" }, - { url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" }, - { url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" }, - { url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" }, - { url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" }, - { url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" }, - { url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" }, - { url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" }, - { url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" }, - { url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" }, - { url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" }, + { 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]] @@ -4730,15 +4620,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.30.0" +version = "2.34.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/4c/af31e0201b48469786ddeb1bf6fd3dfa3a291cc613a0fe6a60163a7535f9/sentry_sdk-2.30.0.tar.gz", hash = "sha256:436369b02afef7430efb10300a344fb61a11fe6db41c2b11f41ee037d2dd7f45", size = 326767, upload-time = "2025-06-12T10:34:34.733Z" } +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/5a/99/31ac6faaae33ea698086692638f58d14f121162a8db0039e68e94135e7f1/sentry_sdk-2.30.0-py2.py3-none-any.whl", hash = "sha256:59391db1550662f746ea09b483806a631c3ae38d6340804a1a4c0605044f6877", size = 343149, upload-time = "2025-06-12T10:34:32.896Z" }, + { 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]] @@ -4898,35 +4788,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, ] -[[package]] -name = "tomli" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, -] - [[package]] name = "tqdm" version = "4.67.1" @@ -4941,14 +4802,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]] @@ -4962,11 +4823,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.14.0" +version = "4.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] [[package]] @@ -5013,31 +4874,31 @@ wheels = [ [[package]] name = "xattr" -version = "1.1.4" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/bf/8b98081f9f8fd56d67b9478ff1e0f8c337cde08bcb92f0d592f0a7958983/xattr-1.1.4.tar.gz", hash = "sha256:b7b02ecb2270da5b7e7deaeea8f8b528c17368401c2b9d5f63e91f545b45d372", size = 16729, upload-time = "2025-01-06T19:19:32.557Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/65/14438ae55acf7f8fc396ee8340d740a3e1d6ef382bf25bf24156cfb83563/xattr-1.2.0.tar.gz", hash = "sha256:a64c8e21eff1be143accf80fd3b8fde3e28a478c37da298742af647ac3e5e0a7", size = 17293, upload-time = "2025-07-14T03:15:44.884Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/5b/f64ba0f93e6447e1997068959f22ff99e08d77dd88d9edcf97ddcb9e9016/xattr-1.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bb4bbe37ba95542081890dd34fa5347bef4651e276647adaa802d5d0d7d86452", size = 23920, upload-time = "2025-01-06T19:17:48.234Z" }, - { url = "https://files.pythonhosted.org/packages/c8/54/ad66655f0b1317b0a297aa2d6ed7d6e5d5343495841fad535bee37a56471/xattr-1.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3da489ecef798705f9a39ea8cea4ead0d1eeed55f92c345add89740bd930bab6", size = 18883, upload-time = "2025-01-06T19:17:49.46Z" }, - { url = "https://files.pythonhosted.org/packages/4d/5d/7d5154570bbcb898e6123c292f697c87c33e12258a1a8b9741539f952681/xattr-1.1.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:798dd0cbe696635a6f74b06fc430818bf9c3b24314e1502eadf67027ab60c9b0", size = 19221, upload-time = "2025-01-06T19:17:51.654Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b7/135cf3018278051f57bb5dde944cb1ca4f7ad4ec383465a08c6a5c7f7152/xattr-1.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2b6361626efad5eb5a6bf8172c6c67339e09397ee8140ec41258737bea9681", size = 39098, upload-time = "2025-01-06T19:17:53.099Z" }, - { url = "https://files.pythonhosted.org/packages/a5/62/577e2eb0108158b78cd93ea3782c7a8d464693f1338a5350a1db16f69a89/xattr-1.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7fa20a0c9ce022d19123b1c5b848d00a68b837251835a7929fe041ee81dcd0", size = 36982, upload-time = "2025-01-06T19:17:54.493Z" }, - { url = "https://files.pythonhosted.org/packages/59/cc/ab3bd7a4bedf445be4b35de4a4627ef2944786724d18eaf28d05c1238c7c/xattr-1.1.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e20eeb08e2c57fc7e71f050b1cfae35cbb46105449853a582bf53fd23c5379e", size = 38891, upload-time = "2025-01-06T19:17:55.853Z" }, - { url = "https://files.pythonhosted.org/packages/45/e8/2285651d92f1460159753fe6628af259c943fcc5071e48a0540fa11dc34d/xattr-1.1.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:477370e75821bded901487e5e752cffe554d1bd3bd4839b627d4d1ee8c95a093", size = 38362, upload-time = "2025-01-06T19:17:57.078Z" }, - { url = "https://files.pythonhosted.org/packages/5f/af/7856c0b1970272a53a428bb20dc125f9fd350fb1b40ebca4e54610af1b79/xattr-1.1.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a8682091cd34a9f4a93c8aaea4101aae99f1506e24da00a3cc3dd2eca9566f21", size = 36724, upload-time = "2025-01-06T19:17:58.534Z" }, - { url = "https://files.pythonhosted.org/packages/5d/34/087e02b32d6288a40b7f6573e97a119016e6c3713d4f4b866bbf56cfb803/xattr-1.1.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2e079b3b1a274ba2121cf0da38bbe5c8d2fb1cc49ecbceb395ce20eb7d69556d", size = 37945, upload-time = "2025-01-06T19:17:59.764Z" }, - { url = "https://files.pythonhosted.org/packages/f0/2a/d0f9e46de4cec5e4aa45fd939549b977c49dd68202fa844d07cb24ce5f17/xattr-1.1.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ae6579dea05bf9f335a082f711d5924a98da563cac72a2d550f5b940c401c0e9", size = 23917, upload-time = "2025-01-06T19:18:00.868Z" }, - { url = "https://files.pythonhosted.org/packages/83/e0/a5764257cd9c9eb598f4648a3658d915dd3520ec111ecbd251b685de6546/xattr-1.1.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd6038ec9df2e67af23c212693751481d5f7e858156924f14340376c48ed9ac7", size = 18891, upload-time = "2025-01-06T19:18:02.029Z" }, - { url = "https://files.pythonhosted.org/packages/8b/83/a81a147987387fd2841a28f767efedb099cf90e23553ead458f2330e47c5/xattr-1.1.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:608b2877526674eb15df4150ef4b70b7b292ae00e65aecaae2f192af224be200", size = 19213, upload-time = "2025-01-06T19:18:03.303Z" }, - { url = "https://files.pythonhosted.org/packages/4b/52/bf093b4eb9873ffc9e9373dcb38ec8a9b5cd4e6a9f681c4c5cf6bf067a42/xattr-1.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54dad1a6a998c6a23edfd25e99f4d38e9b942d54e518570044edf8c767687ea", size = 39302, upload-time = "2025-01-06T19:18:05.846Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d8/9d7315ebae76a7f48bc5e1aecc7e592eb43376a0f6cf470a854d895d2093/xattr-1.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0dab6ff72bb2b508f3850c368f8e53bd706585012676e1f71debba3310acde8", size = 37224, upload-time = "2025-01-06T19:18:07.226Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b2/10eb17bea7e378b2bcd76fc8c2e5158318e2c08e774b13f548f333d7318a/xattr-1.1.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a3c54c6af7cf09432b2c461af257d5f4b1cb2d59eee045f91bacef44421a46d", size = 39145, upload-time = "2025-01-06T19:18:08.403Z" }, - { url = "https://files.pythonhosted.org/packages/74/fb/95bbc28116b3c19a21acc34ec0a5973e9cc97fe49d3f47a65775af3760a8/xattr-1.1.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e346e05a158d554639fbf7a0db169dc693c2d2260c7acb3239448f1ff4a9d67f", size = 38469, upload-time = "2025-01-06T19:18:09.602Z" }, - { url = "https://files.pythonhosted.org/packages/af/03/23db582cb271ed47f2d62956e112501d998b5493f892a77104b5795ae2fc/xattr-1.1.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3ff6d9e2103d0d6e5fcd65b85a2005b66ea81c0720a37036445faadc5bbfa424", size = 36797, upload-time = "2025-01-06T19:18:10.709Z" }, - { url = "https://files.pythonhosted.org/packages/90/c4/b631d0174e097cf8c44d4f70c66545d91dc8ba15bbfa5054dd7da8371461/xattr-1.1.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7a2ee4563c6414dfec0d1ac610f59d39d5220531ae06373eeb1a06ee37cd193f", size = 38128, upload-time = "2025-01-06T19:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e2/bf74df197a415f25e07378bfa301788e3bf2ac029c3a6c7bd56a900934ff/xattr-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:00c26c14c90058338993bb2d3e1cebf562e94ec516cafba64a8f34f74b9d18b4", size = 24246, upload-time = "2025-07-14T03:14:34.873Z" }, + { url = "https://files.pythonhosted.org/packages/a5/51/922df424556ff35b20ca043da5e4dcf0f99cbcb674f59046d08ceff3ebc7/xattr-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b4f43dc644db87d5eb9484a9518c34a864cb2e588db34cffc42139bf55302a1c", size = 19212, upload-time = "2025-07-14T03:14:35.905Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/1ed37812e8285c8002b8834395c53cc89a2d83aa088db642b217be439017/xattr-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7602583fc643ca76576498e2319c7cef0b72aef1936701678589da6371b731b", size = 19546, upload-time = "2025-07-14T03:14:37.242Z" }, + { url = "https://files.pythonhosted.org/packages/d4/b8/ec75db23d81beec68e3be20ea176c11f125697d3bbb5e118b9de9ea7a9ab/xattr-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90c3ad4a9205cceb64ec54616aa90aa42d140c8ae3b9710a0aaa2843a6f1aca7", size = 39426, upload-time = "2025-07-14T03:14:38.264Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9f/c24950641b138072eda7f34d86966dd15cfe3af9a111b5e77b85ee55f99c/xattr-1.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83d87cfe19cd606fc0709d45a4d6efc276900797deced99e239566926a5afedf", size = 37311, upload-time = "2025-07-14T03:14:39.347Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d5/3b7e0dab706d09c6cdb2f05384610e6c5693c72e3794d54a4cad8c838373/xattr-1.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c67dabd9ddc04ead63fbc85aed459c9afcc24abfc5bb3217fff7ec9a466faacb", size = 39222, upload-time = "2025-07-14T03:14:40.768Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/80cf8ec7d92d20b2860c96a1eca18d25e27fa4770f32c9e8250ff32e7386/xattr-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9a18ee82d8ba2c17f1e8414bfeb421fa763e0fb4acbc1e124988ca1584ad32d5", size = 38694, upload-time = "2025-07-14T03:14:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/38/c0/b154b254e6e4596aed3210dd48b2e82d958b16d9a7f65346b9154968d2d0/xattr-1.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:38de598c47b85185e745986a061094d2e706e9c2d9022210d2c738066990fe91", size = 37055, upload-time = "2025-07-14T03:14:43.435Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/3a615660849ef9bdf46d04f9c6d40ee082f7427678013ff85452ed9497db/xattr-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15e754e854bdaac366ad3f1c8fbf77f6668e8858266b4246e8c5f487eeaf1179", size = 38275, upload-time = "2025-07-14T03:14:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/b048a5f6c5a489915026b70b9133242a2a368383ddab24e4e3a5bdba7ebd/xattr-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:daff0c1f5c5e4eaf758c56259c4f72631fa9619875e7a25554b6077dc73da964", size = 24240, upload-time = "2025-07-14T03:14:46.173Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f5/d795774f719a0be6137041d4833ca00b178f816e538948548dff79530f34/xattr-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:109b11fb3f73a0d4e199962f11230ab5f462e85a8021874f96c1732aa61148d5", size = 19218, upload-time = "2025-07-14T03:14:47.412Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8b/65f3bed09ca9ced27bbba8d4a3326f14a58b98ac102163d85b545f81d9c2/xattr-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c7c12968ce0bf798d8ba90194cef65de768bee9f51a684e022c74cab4218305", size = 19539, upload-time = "2025-07-14T03:14:48.413Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/01ecfdf41ce70f7e29c8a21e730de3c157fb1cb84391923581af81a44c45/xattr-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37989dabf25ff18773e4aaeebcb65604b9528f8645f43e02bebaa363e3ae958", size = 39631, upload-time = "2025-07-14T03:14:49.665Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e9/15cbf9c59cf1117e3c45dd429c52f9dab25d95e65ac245c5ad9532986bec/xattr-1.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:165de92b0f2adafb336f936931d044619b9840e35ba01079f4dd288747b73714", size = 37552, upload-time = "2025-07-14T03:14:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f5/cb4dad87843fe79d605cf5d10caad80e2c338a06f0363f1443449185f489/xattr-1.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82191c006ae4c609b22b9aea5f38f68fff022dc6884c4c0e1dba329effd4b288", size = 39472, upload-time = "2025-07-14T03:14:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d9/012df7b814cc4a0ae41afb59ac31d0469227397b29f58c1377e8db0f34ba/xattr-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2b2e9c87dc643b09d86befad218e921f6e65b59a4668d6262b85308de5dbd1dd", size = 38802, upload-time = "2025-07-14T03:14:52.801Z" }, + { url = "https://files.pythonhosted.org/packages/d8/08/e107a5d294a816586f274c33aea480fe740fd446276efc84c067e6c82de2/xattr-1.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:14edd5d47d0bb92b23222c0bb6379abbddab01fb776b2170758e666035ecf3aa", size = 37125, upload-time = "2025-07-14T03:14:54.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6c/a6f9152e10543af67ea277caae7c5a6400a581e407c42156ffce71dd8242/xattr-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12183d5eb104d4da787638c7dadf63b718472d92fec6dbe12994ea5d094d7863", size = 38456, upload-time = "2025-07-14T03:14:55.383Z" }, ] [[package]]