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/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..dbc9c52823 --- /dev/null +++ b/.github/workflows/build-all-tinygrad-models.yaml @@ -0,0 +1,285 @@ +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 }} + steps: + - 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' && (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" + 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..53ce2458f0 --- /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: number + json_version: + description: 'driving_models version number to update (e.g. 5 for driving_models_v5.json)' + required: true + type: number + 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: number + version: + description: 'Minimum selector version' + required: false + type: number +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..45c67267a7 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,13 +16,13 @@ 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: PYTHONWARNINGS: error - 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: @@ -60,7 +59,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - repository: 'commaai/openpilot' + repository: 'sunnypilot/sunnypilot' submodules: true ref: "refs/heads/master" - uses: ./.github/workflows/setup-with-retry 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/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..13d1990968 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,44 @@ 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 && \ 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 +220,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 +235,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 +244,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 +273,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 +303,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..db569f06cb 100644 --- a/.gitignore +++ b/.gitignore @@ -70,8 +70,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..58d0d07a74 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,11 @@ -Version 0.9.10 (2025-06-30) +Version 0.10.0 (2025-07-07) ======================== +* New driving model + * Lead car ground-truth fixes + * Ported over VAE from the MLSIM stack + * New training architecture described in CVPR paper +* Enable live-learned steering actuation delay +* Opt-in audio recording for dashcam video Version 0.9.9 (2025-05-23) ======================== diff --git a/SConstruct b/SConstruct index 77c8525896..2817a46d34 100644 --- a/SConstruct +++ b/SConstruct @@ -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 f47bb9eb81..52c648f388 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; } @@ -198,6 +219,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..1ecefa5adc 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -128,6 +128,7 @@ struct OnroadEvent @0xc4fa6047f024e718 { personalityChanged @91; aeb @92; userFlag @95; + excessiveActuation @96; soundsUnavailableDEPRECATED @47; } @@ -492,7 +493,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 +568,7 @@ struct DeviceState @0xa4d8b5af2aa492eb { chargingDisabledDEPRECATED @18 :Bool; usbOnlineDEPRECATED @12 :Bool; ambientTempCDEPRECATED @30 :Float32; + nvmeTempCDEPRECATED @35 :List(Float32); } struct PandaState @0xa7649e2575e4591e { @@ -585,7 +586,7 @@ struct PandaState @0xa7649e2575e4591e { fanPower @28 :UInt8; fanStallCount @34 :UInt8; - spiChecksumErrorCount @33 :UInt16; + spiErrorCount @33 :UInt16; harnessStatus @21 :HarnessStatus; sbu1Voltage @35 :Float32; @@ -1083,7 +1084,7 @@ struct ModelDataV2 { confidence @23: ConfidenceClass; # Model perceived motion - temporalPose @21 :Pose; + temporalPoseDEPRECATED @21 :Pose; # e2e lateral planner action @26: Action; @@ -2470,13 +2471,19 @@ struct DebugAlert { struct UserFlag { } -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 Touch { @@ -2556,7 +2563,8 @@ struct Event { livestreamDriverEncodeIdx @119 :EncodeIndex; # microphone data - microphone @103 :Microphone; + soundPressure @103 :SoundPressure; + rawAudioData @147 :AudioData; # systems stuff androidLog @20 :AndroidLogEntry; diff --git a/cereal/messaging/tests/test_messaging.py b/cereal/messaging/tests/test_messaging.py index 6234a11c08..2369229980 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: diff --git a/cereal/services.py b/cereal/services.py index 3e72041b72..4418d079a3 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -73,7 +73,8 @@ _services: dict[str, tuple] = { "navThumbnail": (True, 0.), "qRoadEncodeIdx": (False, 20.), "userFlag": (True, 0., 1), - "microphone": (True, 10., 10), + "soundPressure": (True, 10., 10), + "rawAudioData": (False, 20.), # 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/model.h b/common/model.h index 596819256a..90e8096716 100644 --- a/common/model.h +++ b/common/model.h @@ -1 +1 @@ -#define DEFAULT_MODEL "Vegetarian Filet o Fish (Default)" +#define DEFAULT_MODEL "Space Lab 2 (Default)" diff --git a/common/params.cc b/common/params.cc index 0a8821c37a..39592cb905 100644 --- a/common/params.cc +++ b/common/params.cc @@ -103,10 +103,10 @@ Params::~Params() { assert(queue.empty()); } -std::vector Params::allKeys(ParamKeyType type) const { +std::vector Params::allKeys(ParamKeyFlag flag) const { std::vector ret; for (auto &p : keys) { - if (type == ALL || (p.second & type)) { + if (flag == ALL || (p.second.flags & flag)) { ret.push_back(p.first); } } @@ -117,8 +117,16 @@ bool Params::checkKey(const std::string &key) { return keys.find(key) != keys.end(); } +ParamKeyFlag Params::getKeyFlag(const std::string &key) { + return static_cast(keys[key].flags); +} + ParamKeyType Params::getKeyType(const std::string &key) { - return static_cast(keys[key]); + return keys[key].type; +} + +std::optional Params::getKeyDefaultValue(const std::string &key) { + return keys[key].default_value; } int Params::put(const char* key, const char* value, size_t value_size) { @@ -197,17 +205,17 @@ std::map Params::readAll() { return util::read_files_in_dir(getParamPath()); } -void Params::clearAll(ParamKeyType key_type) { +void Params::clearAll(ParamKeyFlag key_flag) { FileLock file_lock(params_path + "/.lock"); - // 1) delete params of key_type + // 1) delete params of key_flag // 2) delete files that are not defined in the keys. if (DIR *d = opendir(getParamPath().c_str())) { struct dirent *de = NULL; while ((de = readdir(d))) { if (de->d_type != DT_DIR) { auto it = keys.find(de->d_name); - if (it == keys.end() || (it->second & key_type)) { + if (it == keys.end() || (it->second.flags & key_flag)) { unlink(getParamPath(de->d_name).c_str()); } } diff --git a/common/params.h b/common/params.h index 73e21a3fee..de4f9b435f 100644 --- a/common/params.h +++ b/common/params.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -9,17 +10,34 @@ #include "common/queue.h" -enum ParamKeyType { +enum ParamKeyFlag { PERSISTENT = 0x02, CLEAR_ON_MANAGER_START = 0x04, CLEAR_ON_ONROAD_TRANSITION = 0x08, CLEAR_ON_OFFROAD_TRANSITION = 0x10, DONT_LOG = 0x20, DEVELOPMENT_ONLY = 0x40, - BACKUP = 0x80, + CLEAR_ON_IGNITION_ON = 0x80, + BACKUP = 0x100, ALL = 0xFFFFFFFF }; +enum ParamKeyType { + STRING = 0, // must be utf-8 decodable + BOOL = 1, + INT = 2, + FLOAT = 3, + TIME = 4, // ISO 8601 + JSON = 5, + BYTES = 6 +}; + +struct ParamKeyAttributes { + uint32_t flags; + ParamKeyType type; + std::optional default_value = std::nullopt; +}; + class Params { public: explicit Params(const std::string &path = {}); @@ -28,16 +46,18 @@ public: Params(const Params&) = delete; Params& operator=(const Params&) = delete; - std::vector allKeys(ParamKeyType type = ALL) const; + std::vector allKeys(ParamKeyFlag flag = ALL) const; bool checkKey(const std::string &key); + ParamKeyFlag getKeyFlag(const std::string &key); ParamKeyType getKeyType(const std::string &key); + std::optional getKeyDefaultValue(const std::string &key); inline std::string getParamPath(const std::string &key = {}) { return params_path + params_prefix + (key.empty() ? "" : "/" + key); } // Delete a value int remove(const std::string &key); - void clearAll(ParamKeyType type); + void clearAll(ParamKeyFlag flag); // helpers for reading values std::string get(const std::string &key, bool block = false); diff --git a/common/params.py b/common/params.py index 66808083dc..494617200f 100644 --- a/common/params.py +++ b/common/params.py @@ -1,5 +1,6 @@ -from openpilot.common.params_pyx import Params, ParamKeyType, UnknownKeyName +from openpilot.common.params_pyx import Params, ParamKeyFlag, ParamKeyType, UnknownKeyName assert Params +assert ParamKeyFlag assert ParamKeyType assert UnknownKeyName diff --git a/common/params_keys.h b/common/params_keys.h index 6abaf08f6b..14ca1dccc9 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -3,209 +3,225 @@ #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, STRING}}, + {"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}}, + {"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, STRING}}, + {"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, STRING}}, + {"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, STRING}}, // 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, STRING}}, + {"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}, + {"HyundaiLongitudinalTuning", {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"}}, + {"LagdToggleDesc", {PERSISTENT, STRING}}, + {"LagdToggleDelay", {PERSISTENT | BACKUP, 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, STRING}}, + {"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, STRING}}, + {"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, ""}}, // Speed Limit Control - {"SpeedLimitControl", PERSISTENT | BACKUP}, - {"SpeedLimitControlPolicy", PERSISTENT | BACKUP}, - {"SpeedLimitEngageType", PERSISTENT | BACKUP}, - {"SpeedLimitOffsetType", PERSISTENT | BACKUP}, - {"SpeedLimitValueOffset", PERSISTENT | BACKUP}, - {"SpeedLimitWarningType", PERSISTENT | BACKUP}, - {"SpeedLimitWarningOffsetType", PERSISTENT | BACKUP}, - {"SpeedLimitWarningValueOffset", PERSISTENT | BACKUP}, + {"SpeedLimitControl", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"SpeedLimitControlPolicy", {PERSISTENT | BACKUP, INT, "3"}}, + {"SpeedLimitEngageType", {PERSISTENT | BACKUP, INT, "0"}}, + {"SpeedLimitOffsetType", {PERSISTENT | BACKUP, INT, "0"}}, + {"SpeedLimitValueOffset", {PERSISTENT | BACKUP, INT, "0"}}, + {"SpeedLimitWarningType", {PERSISTENT | BACKUP, INT, "0"}}, + {"SpeedLimitWarningOffsetType", {PERSISTENT | BACKUP, INT, "0"}}, + {"SpeedLimitWarningValueOffset", {PERSISTENT | BACKUP, INT, "0"}}, }; diff --git a/common/params_pyx.pyx b/common/params_pyx.pyx index e851020d83..1a3e03e7f3 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,14 @@ 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 diff --git a/common/tests/test_params.py b/common/tests/test_params.py index 16cbc45295..1f39769c2a 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,7 +30,7 @@ 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) @@ -37,16 +38,16 @@ class TestParams: 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" + 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..3d03c96bbe 100644 --- a/common/util.cc +++ b/common/util.cc @@ -12,6 +12,7 @@ #include #include #include +#include #ifdef __linux__ #include @@ -78,8 +79,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); diff --git a/common/version.h b/common/version.h index 67b69da7c7..af827e20a6 100644 --- a/common/version.h +++ b/common/version.h @@ -1 +1 @@ -#define COMMA_VERSION "0.9.10" +#define COMMA_VERSION "0.10.0" 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..f2be4c5c9e 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,322 +4,323 @@ 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 +# 313 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 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 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 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|ES 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|ES Hybrid 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|LC 2024-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|NX 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|NX Hybrid 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|NX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RC 2018-20|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RC 2023|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RX 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RX 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RX 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RX Hybrid 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RX Hybrid 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|RX Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lexus|UX Hybrid 2019-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Nissan[7](#footnotes)|Altima 2019-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan B connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Nissan[7](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Nissan[7](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Nissan[7](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|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
||| +|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 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
||| ### 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/opendbc_repo b/opendbc_repo index fb9e07ff49..c5b8a646cc 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit fb9e07ff49a5c6097303af5858fe125f34cc9c22 +Subproject commit c5b8a646ccc550b0f4a73b2e03c671c037de46a2 diff --git a/panda b/panda index c33cfa0803..e46680ff6f 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit c33cfa0803deef5dc53d103ae1a2513e2ab94196 +Subproject commit e46680ff6f8757a89ff82a5d835182725150b592 diff --git a/pyproject.toml b/pyproject.toml index 68e5e35296..b9e2d0ecbf 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,7 @@ 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", + #"rerun-sdk >= 0.18", # this is pretty big, so only enable once we use it ] [project.urls] @@ -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/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/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..083885211f 100644 --- a/selfdrive/car/car_specific.py +++ b/selfdrive/car/car_specific.py @@ -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..0564a4c5bb 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -107,7 +107,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 = json.loads(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 +147,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/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_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..61ad4754a6 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -93,7 +93,8 @@ 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) + calculated_lag = self.LaC.extension.lagd_torqued_main(self.CP, self.sm['liveDelay']) + self.LaC.extension.update_lateral_lag(calculated_lag) long_plan = self.sm['longitudinalPlan'] model_v2 = self.sm['modelV2'] @@ -163,10 +164,7 @@ class Controls(ControlsExt): CC.cruiseControl.override = CC.enabled and not CC.longActive and self.CP.openpilotLongitudinalControl CC.cruiseControl.cancel = CS.cruiseState.enabled and (not CC.enabled or not self.CP.pcmCruise) - - speeds = self.sm['longitudinalPlan'].speeds - if len(speeds): - CC.cruiseControl.resume = CC.enabled and CS.cruiseState.standstill and speeds[-1] > 0.1 + CC.cruiseControl.resume = CC.enabled and CS.cruiseState.standstill and not self.sm['longitudinalPlan'].shouldStop hudControl = CC.hudControl hudControl.setSpeed = float(CS.vCruiseCluster * CV.KPH_TO_MS) diff --git a/selfdrive/controls/lib/drive_helpers.py b/selfdrive/controls/lib/drive_helpers.py index 56eeac3bdf..51eb694b4c 100644 --- a/selfdrive/controls/lib/drive_helpers.py +++ b/selfdrive/controls/lib/drive_helpers.py @@ -1,5 +1,4 @@ import numpy as np -from cereal import log from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY from openpilot.common.realtime import DT_CTRL, DT_MDL @@ -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_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 5edf4cf91b..d7efcbcd8d 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -2,6 +2,7 @@ import math import numpy as np from cereal import log +from opendbc.car import FRICTION_THRESHOLD, get_friction from opendbc.car.interfaces import LatControlInputs from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY from openpilot.selfdrive.controls.lib.latcontrol import LatControl @@ -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) @@ -47,16 +47,9 @@ class LatControlTorque(LatControl): 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 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/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 cc8613b5f7..994aedabad 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -11,7 +11,7 @@ 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 @@ -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 @@ -174,7 +176,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: 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/locationd/lagd.py b/selfdrive/locationd/lagd.py index e6767db473..d7834f7f1f 100755 --- a/selfdrive/locationd/lagd.py +++ b/selfdrive/locationd/lagd.py @@ -157,8 +157,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 +172,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 +206,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,10 +369,7 @@ 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) diff --git a/selfdrive/locationd/models/car_kf.py b/selfdrive/locationd/models/car_kf.py index 349897f371..6db749b949 100755 --- a/selfdrive/locationd/models/car_kf.py +++ b/selfdrive/locationd/models/car_kf.py @@ -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..0066708319 100755 --- a/selfdrive/locationd/torqued.py +++ b/selfdrive/locationd/torqued.py @@ -11,6 +11,8 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.locationd.helpers import PointBuckets, ParameterEstimator, PoseCalibrator, Pose +from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle + HISTORY = 5 # secs POINTS_PER_BUCKET = 1500 MIN_POINTS_TOTAL = 4000 @@ -49,8 +51,10 @@ class TorqueBuckets(PointBuckets): break -class TorqueEstimator(ParameterEstimator): +class TorqueEstimator(ParameterEstimator, LagdToggle): 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 @@ -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 = self.lagd_torqued_main(self.CP, msg) # 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..5c2ce90598 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,15 @@ 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.lagd_toggle import LagdToggle PROCESS_NAME = "selfdrive.modeld.modeld" @@ -46,8 +42,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 +56,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: @@ -86,6 +86,7 @@ class ModelState: prev_desire: np.ndarray # for tracking the rising edge of the pulse def __init__(self, context: CLContext): + 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 +162,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 +187,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 @@ -251,6 +249,8 @@ def main(demo=False): CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) cloudlog.info("modeld got CarParams: %s", CP.brand) + modeld_lagd = LagdToggle() + # TODO this needs more thought, use .2s extra for now to estimate other delays # TODO Move smooth seconds to action function long_delay = CP.longitudinalActuatorDelay + LONG_SMOOTH_SECONDS @@ -296,7 +296,7 @@ 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 + lat_delay = modeld_lagd.lagd_main(CP, sm, model) 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 +374,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..187a3b2ecf 100644 --- a/selfdrive/modeld/models/driving_policy.onnx +++ b/selfdrive/modeld/models/driving_policy.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f714fb38bcd44b5d9f44e3a8e1595e1dfdf7558f0eec3485cf3f2dbb6dc7d8d -size 15971805 +oid sha256:22aec22a10ce09384d4a4af2a0bbff08d54af7e0c888503508f356fae4ff0e29 +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..b89de27594 100644 --- a/selfdrive/modeld/parse_model_outputs.py +++ b/selfdrive/modeld/parse_model_outputs.py @@ -88,24 +88,23 @@ 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) + 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)) + out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) if 'lat_planner_solution' in outs: self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) if 'desired_curvature' in outs: 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..3dbfbcf089 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 @@ -169,7 +168,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 +201,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 +268,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 +337,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 +384,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 +408,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 +421,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 +438,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 +446,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 +463,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 +491,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..cda6a16a4a 100644 --- a/selfdrive/pandad/pandad.h +++ b/selfdrive/pandad/pandad.h @@ -11,7 +11,7 @@ void pandad_main_thread(std::vector serials); 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_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/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..15dd064126 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 has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support.", + "severity": 1, + "_comment": "Set extra field to lateral or longitudinal." } } diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index 9bfc638db3..17e1700100 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -591,6 +591,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"), diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 281014e68e..a61a29ee89 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -7,6 +7,7 @@ import cereal.messaging as messaging from cereal import car, log, custom from msgq.visionipc import VisionIpcClient, VisionStreamType +from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX from openpilot.common.params import Params @@ -15,6 +16,7 @@ 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.state import StateMachine from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroad_alert @@ -30,7 +32,9 @@ 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()} +MIN_EXCESSIVE_ACTUATION_COUNT = int(0.25 / DT_CTRL) ThermalStatus = log.DeviceState.ThermalStatus State = log.SelfdriveState.OpenpilotState @@ -44,6 +48,21 @@ SafetyModel = car.CarParams.SafetyModel IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput) +def check_excessive_actuation(sm: messaging.SubMaster, CS: car.CarState, calibrator: PoseCalibrator, counter: int) -> tuple[int, bool]: + # CS.aEgo can be noisy to bumps in the road, transitioning from standstill, losing traction, etc. + device_pose = Pose.from_live_pose(sm['livePose']) + calibrated_pose = calibrator.build_calibrated_pose(device_pose) + accel_calibrated = calibrated_pose.acceleration.x + + # livePose acceleration can be noisy due to bad mounting or aliased livePose measurements + accel_valid = abs(CS.aEgo - accel_calibrated) < 2 + + excessive_actuation = accel_calibrated > ACCEL_MAX * 2 or accel_calibrated < ACCEL_MIN * 2 + counter = counter + 1 if sm['carControl'].longActive and excessive_actuation and accel_valid else 0 + + return counter, counter > MIN_EXCESSIVE_ACTUATION_COUNT + + class SelfdriveD(CruiseHelper): def __init__(self, CP=None, CP_SP=None): self.params = Params() @@ -66,6 +85,7 @@ class SelfdriveD(CruiseHelper): self.CP_SP = CP_SP self.car_events = CarSpecificEvents(self.CP) + self.calibrator = PoseCalibrator() # Setup sockets self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents'] + ['selfdriveStateSP', 'onroadEventsSP']) @@ -121,8 +141,10 @@ 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.excessive_actuation = self.params.get("Offroad_ExcessiveActuation") is not None + self.excessive_actuation_counter = 0 self.state_machine = StateMachine() self.rk = Ratekeeper(100, print_delay_threshold=None) @@ -135,7 +157,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: @@ -254,6 +277,18 @@ class SelfdriveD(CruiseHelper): if self.sm['driverAssistance'].leftLaneDeparture or self.sm['driverAssistance'].rightLaneDeparture: self.events.add(EventName.ldw) + # Check for excessive (longitudinal) actuation + if self.sm.updated['liveCalibration']: + self.calibrator.feed_live_calib(self.sm['liveCalibration']) + + self.excessive_actuation_counter, excessive_actuation = check_excessive_actuation(self.sm, CS, self.calibrator, self.excessive_actuation_counter) + if not self.excessive_actuation and excessive_actuation: + set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text="longitudinal") + 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 @@ -407,13 +442,13 @@ class SelfdriveD(CruiseHelper): if any(not be.pressed and be.type == ButtonType.gapAdjustCruise for be in CS.buttonEvents): if not self.experimental_mode_switched: self.personality = (self.personality - 1) % 3 - self.params.put_nonblocking('LongitudinalPersonality', str(self.personality)) + self.params.put_nonblocking('LongitudinalPersonality', self.personality) self.events.add(EventName.personalityChanged) self.experimental_mode_switched = False def data_sample(self): - car_state = messaging.recv_one(self.car_state_sock) - CS = car_state.carState if car_state else self.CS_prev + _car_state = messaging.recv_one(self.car_state_sock) + CS = _car_state.carState if _car_state else self.CS_prev self.sm.update(0) @@ -538,19 +573,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/ref_commit b/selfdrive/test/process_replay/ref_commit index f839238098..92669bec5c 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -f440c9e0469d32d350aa99ddaa8f44591a2ce690 \ No newline at end of file +c289a0359d1b1f26cf4d9e73a2c04b2bbfec840f \ No newline at end of file diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index f98e3bd88c..cd702377e7 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -147,7 +147,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..5cff91b447 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -72,14 +72,10 @@ if GetOption('extras'): 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) + # setup 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) - if arch != "Darwin": # build installers raylib_env = env.Clone() 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..144d34e02f 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): 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..a322f21e80 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 @@ -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..58afcec5ef 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,7 @@ 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.", } @@ -53,7 +54,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 +76,13 @@ 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( + "Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="metric.png" ), ] @@ -85,4 +92,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..82d499716d 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) @@ -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() @@ -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..cc2c6b4fe7 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 @@ -179,6 +179,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 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..e6748e4e09 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 @@ -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..a74d309e02 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.conversions import Conversions as CV from openpilot.selfdrive.ui.onroad.exp_button import ExpButton +from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.common.conversions import Conversions as CV -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget # Constants SET_SPEED_NA = 255 diff --git a/selfdrive/ui/onroad/model_renderer.py b/selfdrive/ui/onroad/model_renderer.py index ab2b7aaefb..4439141a40 100644 --- a/selfdrive/ui/onroad/model_renderer.py +++ b/selfdrive/ui/onroad/model_renderer.py @@ -4,11 +4,11 @@ import pyray as rl from cereal import messaging, car from dataclasses import dataclass, field from openpilot.common.params import Params +from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import DEFAULT_FPS from openpilot.system.ui.lib.shader_polygon import draw_polygon -from openpilot.system.ui.lib.widget import Widget -from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT +from openpilot.system.ui.widgets import Widget CLIP_MARGIN = 500 MIN_DRAW_DISTANCE = 10.0 diff --git a/selfdrive/ui/qt/offroad/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..205a14624a 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -68,6 +68,13 @@ 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, + }, { "IsMetric", tr("Use Metric System"), @@ -342,26 +349,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 +375,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/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/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..c63cc01592 100644 --- a/selfdrive/ui/qt/sidebar.cc +++ b/selfdrive/ui/qt/sidebar.cc @@ -24,10 +24,11 @@ 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)); connect(this, &Sidebar::valueChanged, [=] { update(); }); @@ -47,12 +48,15 @@ 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())) { @@ -61,6 +65,8 @@ void Sidebar::mouseReleaseEvent(QMouseEvent *event) { pm->send("userFlag", 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 +112,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 +132,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 diff --git a/selfdrive/ui/qt/sidebar.h b/selfdrive/ui/qt/sidebar.h index 3c57d10a1a..4f8409be10 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; + 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..43ba7e037f 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") { + 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..45a22d7e04 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,11 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { connect(uiStateSP(), &UIStateSP::uiUpdate, this, &ModelsPanel::updateLabels); list->addItem(currentModelLblBtn); + 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 +97,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 +251,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 +356,24 @@ 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."); + QString current = QString::fromStdString(params.get("LagdToggleDesc", false)); + if (!current.isEmpty()) { + desc += "

" + tr("Current:") + " " + current + ""; + } + lagd_toggle_control->setDescription(desc); + + delay_control->setVisible(!params.getBool("LagdToggle")); + if (delay_control->isVisible()) { + float value = QString::fromStdString(params.get("LagdToggleDelay")).toFloat(); + delay_control->setLabel(QString::number(value, 'f', 2) + "s"); + } + + clearModelCacheBtn->setValue(QString::number(calculateCacheSize(), 'f', 2) + " MB"); } /** @@ -310,3 +394,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..84a2f56e05 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,6 @@ private: QProgressBar *policyProgressBar; QFrame *policyFrame; Params params; + ButtonControlSP *clearModelCacheBtn; }; 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/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_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index b263e28297..33f33f98dd 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -408,9 +408,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..7d3d9edd05 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 إعادة التشغيل والتحديث @@ -107,34 +103,6 @@ - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - - - ConfirmationDialog @@ -149,8 +117,8 @@ 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. + You must accept the Terms and Conditions in order to use openpilot. + يجب عليك قبول الشروط والأحكام من أجل استخدام openpilot. Back @@ -171,6 +139,18 @@ 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). + + + On this car, openpilot 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. + في هذه السيارة يعمل openpilot افتراضياً بالشكل المدمج في التحكم التكيفي في السرعة بدلاً من التحكم الطولي. قم بتمكين هذا الخيار من أجل الانتقال إلى التحكم الطولي. يوصى بتمكين الوضع التجريبي عند استخدام وضع التحكم الطولي ألفا من openpilot. + Enable ADB تمكين ADB @@ -179,38 +159,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. أداة ADB (Android Debug Bridge) تسمح بالاتصال بجهازك عبر USB أو عبر الشبكة. راجع هذا الرابط: https://docs.comma.ai/how-to/connect-to-comma لمزيد من المعلومات. - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Error Log - - - - VIEW - عرض - - - View the error log for sunnypilot crashes. - - - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - DevicePanel @@ -259,8 +207,8 @@ Please use caution when using this feature. Only use the blinker when traffic an مراجعة - Review the rules, features, and limitations of sunnypilot - مراجعة الأدوار والميزات والقيود في sunnypilot + Review the rules, features, and limitations of openpilot + مراجعة الأدوار والميزات والقيود في openpilot Are you sure you want to review the training guide? @@ -294,10 +242,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,121 +303,31 @@ 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. - - - - - DevicePanelSP - - Driver Camera Preview + openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - Training Guide + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - Regulatory - التنظيمية - - - Language + + +Steering lag calibration is %1% complete. - Are you sure you want to review the training guide? - هل أنت متأكد أنك تريد مراجعة دليل التدريب؟ - - - Review - مراجعة - - - Select a language - اختر لغة - - - Reboot - إعادة التشغيل - - - Power Off - إيقاف التشغيل - - - Offroad Mode + + +Steering lag calibration is complete. - Are you sure you want to exit Always Offroad mode? + Steering torque response calibration is %1% complete. - Confirm - تأكيد - - - Are you sure you want to enter Always Offroad mode? - - - - 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. - - - - Reset - إعادة الضبط - - - The reset cannot be undone. You have been warned. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles + Steering torque response calibration is complete. @@ -498,8 +352,12 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - 🔥 Firehose Mode 🔥 - 🔥 وضع خرطوم الحريق 🔥 + openpilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + يتعلم تطبيق openpilot كيفية القيادة من خلال مشاهدة البشر، مثلك، أثناء القيادة. + +يتيح لك وضع خرطوم الحريق زيادة تحميلات بيانات التدريب لتحسين نماذج القيادة في OpenPilot. كلما زادت البيانات، زادت النماذج، مما يعني وضعًا تجريبيًا أفضل. Firehose Mode: ACTIVE @@ -510,8 +368,8 @@ Please use caution when using this feature. Only use the blinker when traffic an نشط - 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 (وأفرع معينة) يمكن استخدامها للتدريب. + 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 openpilot (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> نعم، فقط النسخة الأصلية من openpilot (وأفرع معينة) يمكن استخدامها للتدريب. <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -525,13 +383,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + <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;'>INACTIVE</span>: connect to an unmetered network + Firehose Mode @@ -550,49 +406,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp MAX - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - InputDialog @@ -611,265 +424,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - LaneChangeSettings - - Back - السابق - - - Auto Lane Change: Delay with Blind Spot - - - - Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. - - - - - LateralPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. - - - - Customize MADS - - - - Customize Lane Change - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - - MadsSettings - - Toggle with Main Cruise - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Unified Engagement Mode (UEM) - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - 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. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - اختيار - - - No custom model selected! - - - - Driving - - - - Navigation - - - - Vision - - - - Policy - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - إعادة ضبط المعايرة - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - متابعة - - - on Metered - - - - Cancel - إلغاء - - MultiOptionDialog @@ -900,76 +454,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp كلمة مرور خاطئة - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - OffroadAlert @@ -977,12 +461,12 @@ Firehose Mode allows you to maximize your training data uploads to improve openp درجة حرارة الجهاز مرتفعة جداً. يقوم النظام بالتبريد قبل البدء. درجة الحرارة الحالية للمكونات الداخلية: %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 + Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 + اتصل فوراً بالإنترنت للتحقق من وجود تحديثات. إذا لم تكم متصلاً بالإنترنت فإن openpilot لن يساهم في %1 - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - اتصل بالإنترنت للتحقق من وجود تحديثات. لا يعمل sunnypilot تلقائياً إلا إذا اتصل بالإنترنت من أجل التحقق من التحديثات. + Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. + اتصل بالإنترنت للتحقق من وجود تحديثات. لا يعمل openpilot تلقائياً إلا إذا اتصل بالإنترنت من أجل التحقق من التحديثات. Unable to download updates @@ -998,28 +482,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. - فشل تسجيل الجهاز. لن يقوم بالاتصال أو تحميل خوادم 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 غير مدعوم. + openpilot 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. + لم يكن openpilot قادراً على تحديد سيارتك. إما أن تكون سيارتك غير مدعومة أو أنه لم يتم التعرف على وحدة التحكم الإلكتروني (ECUs) فيها. يرجى تقديم طلب سحب من أجل إضافة نسخ برمجيات ثابتة إلى السيارة المناسبة. هل تحتاج إلى أي مساعدة؟ لا تتردد في التواصل مع doscord.comma.ai. - 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. + openpilot 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. + لقد اكتشف openpilot تغييراً في موقع تركيب الجهاز. تأكد من تثبيت الجهاز بشكل كامل في موقعه وتثبيته بإحكام على الزجاج الأمامي. - 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 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. + - 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. + Acknowledge Excessive Actuation + + + + Snooze Update + تأخير التحديث + + + openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. @@ -1041,8 +529,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OnroadAlerts - sunnypilot Unavailable - sunnypilot غير متوفر + openpilot Unavailable + openpilot غير متوفر TAKE CONTROL IMMEDIATELY @@ -1095,96 +583,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp إلغاء - - ParamControlSP - - Enable - تمكين - - - Cancel - إلغاء - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - إزالة - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - تأكيد - - - Cancel - إلغاء - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - PrimeAdWidget @@ -1230,8 +628,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - sunnypilot - sunnypilot + openpilot + openpilot %n minute(s) ago @@ -1271,47 +669,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp الآن - - 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. - تم تفعيل إعادة ضبط النظام. اضغط على تأكيد لمسح جميع المحتويات والإعدادات. اضغط على إلغاء لاستئناف التمهيد. - - SettingsWindow @@ -1343,65 +700,6 @@ This may take up to a minute. خرطوم الحريق - - SettingsWindowSP - - × - × - - - Device - الجهاز - - - Network - الشبكة - - - sunnylink - - - - Toggles - المثبتتات - - - Software - البرنامج - - - Trips - - - - Vehicle - - - - Developer - المطور - - - Firehose - خرطوم الحريق - - - Steering - - - - Models - - - - Cruise - - - - Visuals - - - Setup @@ -1489,13 +787,23 @@ This may take up to a minute. اختر البرنامج للتثبيت - sunnypilot - sunnypilot + openpilot + openpilot Custom Software البرمجيات المخصصة + + WARNING: Custom Software + + + + Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. + +If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + + SetupWidget @@ -1587,33 +895,6 @@ This may take up to a minute. 5G - - SidebarSP - - DISABLED - - - - OFFLINE - غير متصل - - - REGIST... - - - - ONLINE - متصل - - - ERROR - خطأ - - - SUNNYLINK - - - SoftwarePanel @@ -1689,25 +970,6 @@ This may take up to a minute. أحدث نسخة، آخر تحقق %1 - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - No branches found for keywords: %1 - - - - Select a branch - اختر فرعاً - - SshControl @@ -1754,168 +1016,6 @@ This may take up to a minute. تمكين SSH - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - 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 - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - إقران - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - THANKS ♥ - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - TermsPage @@ -1927,16 +1027,20 @@ This may take up to a minute. أوافق - Welcome to sunnypilot - مرحباً بكم في sunnypilot + Welcome to openpilot + مرحباً بكم في openpilot - 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> قبل الاستمرار. + You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + يجب عليك قبول الشروط والأحكام لاستخدام openpilot. اقرأ أحدث الشروط على <span style='color: #465BEA;'>https://comma.ai/terms</span> قبل الاستمرار. TogglesPanel + + Enable openpilot + تمكين openpilot + Enable Lane Departure Warnings قم بتمكين تحذيرات مغادرة المسار @@ -1966,8 +1070,8 @@ This may take up to a minute. فك الارتباط عن دواسة الوقود - When enabled, pressing the accelerator pedal will disengage sunnypilot. - عند تمكين هذه الميزة، فإن الضغط على دواسة الوقود سيؤدي إلى فك ارتباط sunnypilot. + When enabled, pressing the accelerator pedal will disengage openpilot. + عند تمكين هذه الميزة، فإن الضغط على دواسة الوقود سيؤدي إلى فك ارتباط openpilot. Experimental Mode @@ -1990,16 +1094,16 @@ This may take up to a minute. شخصية القيادة - 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> التي لا تكون جاهزة في وضع الراحة: + openpilot 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: + يتم وضع openpilot بشكل قياسي في <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 بالقيادة كما لو أنه كائن بشري، بما في ذلك التوقف عند الإشارة الحمراء، وإشارات التوقف. وبما أن نمط القيادة يحدد سرعة القيادة، فإن السرعة المضبوطة تشكل الحد الأقصى فقط. هذه خاصية الجودة ألفا، فيجب توقع حدوث الأخطاء. + Let the driving model control the gas and brakes. openpilot 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. + دع نظام القيادة يتحكم بالوقود والمكابح. سيقوم openpilot بالقيادة كما لو أنه كائن بشري، بما في ذلك التوقف عند الإشارة الحمراء، وإشارات التوقف. وبما أن نمط القيادة يحدد سرعة القيادة، فإن السرعة المضبوطة تشكل الحد الأقصى فقط. هذه خاصية الجودة ألفا، فيجب توقع حدوث الأخطاء. New Driving Visualization @@ -2010,16 +1114,20 @@ 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، مع الوضع التجريبي، لكن على الفروع غير المطلقة. + openpilot longitudinal control may come in a future update. + قد يتم الحصول على التحكم الطولي في openpilot في عمليات التحديث المستقبلية. - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - تمكين التحكم الطولي من sunnypilot (ألفا) للسماح بالوضع التجريبي. + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + يمكن اختبار نسخة ألفا من التحكم الطولي من openpilot، مع الوضع التجريبي، لكن على الفروع غير المطلقة. - 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 بعيدًا عن السيارات الرائدة. في السيارات المدعومة، يمكنك التنقل بين هذه الشخصيات باستخدام زر مسافة عجلة القيادة. + Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. + تمكين التحكم الطولي من openpilot (ألفا) للسماح بالوضع التجريبي. + + + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + يوصى بالمعيار. في الوضع العدواني، سيتبع الطيار المفتوح السيارات الرائدة بشكل أقرب ويكون أكثر عدوانية مع البنزين والفرامل. في الوضع المريح، سيبقى 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,23 +1138,11 @@ This may take up to a minute. مراقبة السائق المستمرة - Enable driver monitoring even when sunnypilot is not engaged. + Enable driver monitoring even when openpilot is not engaged. تمكين مراقبة السائق حتى عندما لا يكون نظام OpenPilot مُفعّلاً. - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - 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. + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. @@ -2054,43 +1150,12 @@ This may take up to a minute. - openpilot longitudinal control may come in a future update. + Record and Upload Microphone Audio - - - Updater - Update Required - التحديث مطلوب - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - تحديث نظام التشغيل مطلوب. قم بوصل جهازك بشبكة واي فاي من أجل تحديث أسرع. حجم التحميل حوالي 1 غيغا بايت تقريباً. - - - Connect to Wi-Fi - الاتصال بشبكة الواي فاي - - - Install - تثبيت - - - Back - السابق - - - Loading... - يتم التحميل... - - - Reboot - إعادة التشغيل - - - Update failed - فشل التحديث + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + @@ -2100,12 +1165,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..3489b4860e 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 @@ -107,34 +103,6 @@ - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - - - ConfirmationDialog @@ -148,6 +116,10 @@ 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 openpilot. + Du musst die Nutzungsbedingungen akzeptieren, um Openpilot zu benutzen. + Back Zurück @@ -156,10 +128,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 Ablehnen, deinstallieren %1 - - You must accept the Terms and Conditions in order to use sunnypilot. - - DeveloperPanel @@ -179,6 +147,10 @@ Please use caution when using this feature. Only use the blinker when traffic an WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). WARNUNG: Die openpilot Längsregelung befindet sich für dieses Fahrzeug im Alpha-Stadium und deaktiviert das automatische Notbremsen (AEB). + + On this car, openpilot 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. + Bei diesem Fahrzeug verwendet openpilot standardmäßig den eingebauten Tempomaten anstelle der openpilot Längsregelung. Aktiviere diese Option, um auf die openpilot Längsregelung umzuschalten. Es wird empfohlen, den experimentellen Modus zu aktivieren, wenn die openpilot Längsregelung (Alpha) aktiviert wird. + Enable ADB ADB aktivieren @@ -187,30 +159,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB (Android Debug Bridge) ermöglicht die Verbindung zu deinem Gerät über USB oder Netzwerk. Siehe https://docs.comma.ai/how-to/connect-to-comma für weitere Informationen. - - 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. - - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Error Log - - - - VIEW - ANSEHEN - - - View the error log for sunnypilot crashes. - - DevicePanel @@ -258,6 +206,10 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW TRAINING + + Review the rules, features, and limitations of openpilot + Wiederhole die Regeln, Fähigkeiten und Limitierungen von Openpilot + Are you sure you want to review the training guide? Bist du sicher, dass du die Trainingsanleitung wiederholen möchtest? @@ -351,129 +303,31 @@ Please use caution when using this feature. Only use the blinker when traffic an - Review the rules, features, and limitations of sunnypilot + openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - 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. + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - Resetting calibration will restart openpilot if the car is powered on. - - - - - DevicePanelSP - - Quiet Mode + + +Steering lag calibration is %1% complete. - Driver Camera Preview + + +Steering lag calibration is complete. - Training Guide + Steering torque response calibration is %1% complete. - Regulatory - Rechtliche Hinweise - - - Language - - - - Reset Settings - - - - Are you sure you want to review the training guide? - Bist du sicher, dass du die Trainingsanleitung wiederholen möchtest? - - - Review - Überprüfen - - - Select a language - Sprache wählen - - - Reboot - Neustart - - - Power Off - Ausschalten - - - Offroad Mode - - - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - Bestätigen - - - Are you sure you want to enter Always Offroad mode? - - - - Disengage to Enter Always Offroad Mode - - - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. - - - - Reset - Zurücksetzen - - - The reset cannot be undone. You have been warned. - - - - Exit Always Offroad - - - - Always Offroad - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles + Steering torque response calibration is complete. @@ -498,8 +352,12 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - 🔥 Firehose Mode 🔥 - 🔥 Firehose-Modus 🔥 + openpilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + openpilot lernt das Fahren, indem es Menschen wie dir beim Fahren zuschaut. + +Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximieren, um die Fahrmodelle von openpilot zu verbessern. Mehr Daten bedeuten größere Modelle, was zu einem besseren Experimentellen Modus führt. Firehose Mode: ACTIVE @@ -509,6 +367,10 @@ Please use caution when using this feature. Only use the blinker when traffic an ACTIVE AKTIV + + 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 openpilot (and particular forks) are able to be used for training. + Für maximale Effektivität bring dein Gerät jede Woche nach drinnen und verbinde es mit einem guten USB-C-Adapter und WLAN.<br><br>Der Firehose-Modus funktioniert auch während der Fahrt, wenn das Gerät mit einem Hotspot oder einer ungedrosselten SIM-Karte verbunden ist.<br><br><br><b>Häufig gestellte Fragen</b><br><br><i>Spielt es eine Rolle, wie oder wo ich fahre?</i> Nein, fahre einfach wie gewohnt.<br><br><i>Werden im Firehose-Modus alle meine Segmente hochgeladen?</i> Nein, wir wählen selektiv nur einen Teil deiner Segmente aus.<br><br><i>Welcher USB-C-Adapter ist gut?</i> Jedes Schnellladegerät für Handy oder Laptop sollte ausreichen.<br><br><i>Spielt es eine Rolle, welche Software ich nutze?</i> Ja, nur das offizielle Upstream‑openpilot (und bestimmte Forks) kann für das Training verwendet werden. + <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -521,13 +383,7 @@ Please use caution when using this feature. Only use the blinker when traffic an <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INAKTIV</span>: Verbinde dich mit einem ungedrosselten Netzwerk - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + Firehose Mode @@ -546,49 +402,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp MAX - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - InputDialog @@ -603,265 +416,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - LaneChangeSettings - - Back - Zurück - - - Auto Lane Change: Delay with Blind Spot - - - - Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. - - - - - LateralPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. - - - - Customize MADS - - - - Customize Lane Change - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - - MadsSettings - - Toggle with Main Cruise - - - - Unified Engagement Mode (UEM) - - - - Steering Mode on Brake Pedal - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - 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. - - - - Start the vehicle to check vehicle compatibility. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - AUSWÄHLEN - - - No custom model selected! - - - - Driving - - - - Navigation - - - - Vision - - - - Policy - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Neu kalibrieren - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - Fortsetzen - - - on Metered - - - - Cancel - Abbrechen - - MultiOptionDialog @@ -892,78 +446,16 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Falsches Passwort - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - OffroadAlert + + Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 + Stelle sofort eine Internetverbindung her, um nach Updates zu suchen. Wenn du keine Verbindung herstellst, kann openpilot in %1 nicht mehr aktiviert werden. + + + Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. + Verbinde dich mit dem Internet, um nach Updates zu suchen. openpilot startet nicht automatisch, bis eine Internetverbindung besteht und nach Updates gesucht wurde. + Unable to download updates %1 @@ -978,40 +470,36 @@ 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. + openpilot 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. + openpilot konnte dein Auto nicht identifizieren. Dein Auto wird entweder nicht unterstützt oder die Steuergeräte (ECUs) werden nicht erkannt. Bitte reiche einen Pull Request ein, um die Firmware-Versionen für das richtige Fahrzeug hinzuzufügen. Hilfe findest du auf discord.comma.ai. + + + openpilot 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. + openpilot hat eine Änderung der Montageposition des Geräts erkannt. Stelle sicher, dass das Gerät vollständig in der Halterung sitzt und die Halterung fest an der Windschutzscheibe befestigt ist. 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 - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %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. - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + Acknowledge Excessive Actuation - 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. - + Snooze Update + Update pausieren - 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 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. + openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. @@ -1032,6 +520,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OnroadAlerts + + openpilot Unavailable + openpilot nicht verfügbar + TAKE CONTROL IMMEDIATELY ÜBERNIMM SOFORT DIE KONTROLLE @@ -1048,10 +540,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive System reagiert nicht - - sunnypilot Unavailable - - PairingPopup @@ -1087,96 +575,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Aktivieren - - ParamControlSP - - Enable - Aktivieren - - - Cancel - Abbrechen - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - LÖSCHEN - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - Bestätigen - - - Cancel - Abbrechen - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - PrimeAdWidget @@ -1221,6 +619,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject + + openpilot + openpilot + %n minute(s) ago @@ -1246,51 +648,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp now jetzt - - sunnypilot - - - - - 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 @@ -1323,65 +680,6 @@ Dies kann bis zu einer Minute dauern. Firehose - - SettingsWindowSP - - × - x - - - Device - Gerät - - - Network - Netzwerk - - - sunnylink - - - - Toggles - Schalter - - - Software - Software - - - Models - - - - Steering - - - - Cruise - - - - Trips - - - - Vehicle - - - - Firehose - Firehose - - - Developer - Entwickler - - - Visuals - - - Setup @@ -1468,12 +766,22 @@ Dies kann bis zu einer Minute dauern. Choose Software to Install Wähle die zu installierende Software + + openpilot + openpilot + Custom Software Benutzerdefinierte Software - sunnypilot + WARNING: Custom Software + + + + Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. + +If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. @@ -1568,33 +876,6 @@ Dies kann bis zu einer Minute dauern. 5G - - SidebarSP - - DISABLED - - - - OFFLINE - OFFLINE - - - REGIST... - - - - ONLINE - ONLINE - - - ERROR - FEHLER - - - SUNNYLINK - - - SoftwarePanel @@ -1671,25 +952,6 @@ Dies kann bis zu einer Minute dauern. nie - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - No branches found for keywords: %1 - - - - Select a branch - Wähle einen Branch - - SshControl @@ -1736,168 +998,6 @@ Dies kann bis zu einer Minute dauern. SSH aktivieren - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - Enable sunnylink - - - - Sponsor Status - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - KOPPELN - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - N/A - Nicht verfügbar - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - 🎉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 🎉. - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - 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 - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - TermsPage @@ -1909,16 +1009,20 @@ Dies kann bis zu einer Minute dauern. Zustimmen - Welcome to sunnypilot - + Welcome to openpilot + Willkommen bei openpilot - 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. - + You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + Du musst die Nutzungsbedingungen akzeptieren, um openpilot zu verwenden. Lies die aktuellen Bedingungen unter <span style='color: #465BEA;'>https://comma.ai/terms</span>, bevor du fortfährst. TogglesPanel + + Enable openpilot + Openpilot aktivieren + Enable Lane Departure Warnings Spurverlassenswarnungen aktivieren @@ -1943,6 +1047,10 @@ Dies kann bis zu einer Minute dauern. Upload data from the driver facing camera and help improve the driver monitoring algorithm. Lade Daten der Fahreraufmerksamkeitsüberwachungskamera hoch, um die Fahreraufmerksamkeitsüberwachungsalgorithmen zu verbessern. + + When enabled, pressing the accelerator pedal will disengage openpilot. + Wenn aktiviert, deaktiviert sich Openpilot sobald das Gaspedal betätigt wird. + Experimental Mode Experimenteller Modus @@ -1951,6 +1059,14 @@ Dies kann bis zu einer Minute dauern. Disengage on Accelerator Pedal Bei Gasbetätigung ausschalten + + openpilot 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: + Openpilot fährt standardmäßig im <b>entspannten Modus</b>. Der Experimentelle Modus aktiviert<b>Alpha-level Funktionen</b>, die noch nicht für den entspannten Modus bereit sind. Die experimentellen Funktionen sind die Folgenden: + + + Let the driving model control the gas and brakes. openpilot 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. + Lass das Fahrmodell Gas und Bremse kontrollieren. Openpilot wird so fahren, wie es dies von einem Menschen erwarten würde; inklusive des Anhaltens für Ampeln und Stoppschildern. Da das Fahrmodell entscheidet wie schnell es fährt stellt die gesetzte Geschwindigkeit lediglich das obere Limit dar. Dies ist ein Alpha-level Funktion. Fehler sind zu erwarten. + New Driving Visualization Neue Fahrvisualisierung @@ -1983,6 +1099,18 @@ Dies kann bis zu einer Minute dauern. openpilot longitudinal control may come in a future update. Die openpilot Längsregelung könnte in einem zukünftigen Update verfügbar sein. + + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + Eine Alpha-Version der openpilot Längsregelung kann zusammen mit dem Experimentellen Modus auf non-stable Branches getestet werden. + + + Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. + Aktiviere den Schalter für openpilot Längsregelung (Alpha), um den Experimentellen Modus zu erlauben. + + + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + Standard wird empfohlen. Im aggressiven Modus folgt openpilot vorausfahrenden Fahrzeugen enger und ist beim Gasgeben und Bremsen aggressiver. Im entspannten Modus hält openpilot mehr Abstand zu vorausfahrenden Fahrzeugen. Bei unterstützten Fahrzeugen kannst du mit der Abstandstaste am Lenkrad zwischen diesen Fahrstilen wechseln. + 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. Die Fahrvisualisierung wechselt bei niedrigen Geschwindigkeiten auf die nach vorne gerichtete Weitwinkelkamera, um Kurven besser darzustellen. Das Logo des Experimentellen Modus wird außerdem oben rechts angezeigt. @@ -1992,31 +1120,11 @@ Dies kann bis zu einer Minute dauern. Dauerhaft aktive Fahrerüberwachung - Enable sunnypilot - + Enable driver monitoring even when openpilot is not engaged. + Fahrerüberwachung auch aktivieren, wenn openpilot nicht aktiv ist. - 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 - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - 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. + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. @@ -2024,56 +1132,13 @@ Dies kann bis zu einer Minute dauern. - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + Record and Upload Microphone Audio - 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. + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - - 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 - - Update Required - Aktualisierung notwendig - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - Eine Aktualisierung des Betriebssystems ist notwendig. Verbinde dein Gerät mit WLAN für ein schnelleres Update. Die Download Größe ist ungefähr 1GB. - - - Connect to Wi-Fi - Mit WLAN verbinden - - - Install - Installieren - - - Back - Zurück - - - Loading... - Laden... - - - Reboot - Neustart - - - Update failed - Aktualisierung fehlgeschlagen - WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 3d9622166a..5807accb92 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,55 +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 - - - - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - + Evite cargas de grandes cantidades de datos cuando esté en una conexión Wi-Fi medida @@ -149,8 +117,8 @@ 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. + You must accept the Terms and Conditions in order to use openpilot. + Debe aceptar los términos y condiciones para poder utilizar openpilot. Back @@ -171,45 +139,25 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode Modo de maniobra longitudinal - - Enable ADB - - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Error Log - - - - VIEW - VER - - - View the error log for sunnypilot crashes. - - 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). - 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. - + On this car, openpilot 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. + En este automóvil, openpilot se configura de manera predeterminada con el Autocrucero Adaptativo (ACC) incorporado en el automóvil en lugar del control longitudinal de openpilot. Habilita esta opción para cambiar al control longitudinal de openpilot. Se recomienda activar el modo experimental al habilitar el control longitudinal de openpilot (aún en fase experimental). + + + 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. @@ -275,8 +223,8 @@ Please use caution when using this feature. Only use the blinker when traffic an REVISAR - Review the rules, features, and limitations of sunnypilot - Revisar las reglas, características y limitaciones de sunnypilot + Review the rules, features, and limitations of openpilot + Revisar las reglas, características y limitaciones de openpilot Are you sure you want to review the training guide? @@ -314,10 +262,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,125 +300,39 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Reset Calibration - + Desactivar para restablecer la calibración - Resetting calibration will restart openpilot if the car is powered on. - - - - - DevicePanelSP - - Driver Camera Preview - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. + openpilot requiere que el dispositivo esté montado dentro de los 4° hacia la izquierda o la derecha y dentro de los 5° hacia arriba o 9° hacia abajo. - Training Guide - + 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. - Regulatory - Regulador + + +Steering lag calibration is %1% complete. + + +La calibración del retraso de dirección está %1% completada. - Language - + + +Steering lag calibration is complete. + + +La calibración del retraso de la dirección está completa. - Are you sure you want to review the training guide? - ¿Seguro que quiere revisar la guía de entrenamiento? + Steering torque response calibration is %1% complete. + La calibración de la respuesta del par de dirección está %1% completada. - Review - Revisar - - - Select a language - - - - Reboot - Reiniciar - - - Power Off - Apagar - - - Offroad Mode - - - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - Confirmar - - - Are you sure you want to enter Always Offroad mode? - - - - 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. - - - - Reset - Formatear - - - The reset cannot be undone. You have been warned. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - + Steering torque response calibration is complete. + La calibración de la respuesta del par de dirección está completa. @@ -498,37 +356,39 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - 🔥 Firehose Mode 🔥 - + openpilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + openpilot aprende a conducir observando a humanos, como tú, conducir. + +El Modo Firehose te permite maximizar las subidas de datos de entrenamiento para mejorar los modelos de conducción de openpilot. Más datos significan modelos más grandes, lo que significa un mejor Modo Experimental. Firehose Mode: ACTIVE - + Modo Firehose: ACTIVO ACTIVE - + ACTIVO - 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. - + 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 openpilot (and particular forks) are able to be used for training. + Para máxima efectividad, traiga su dispositivo adentro y conéctelo a un buen adaptador USB-C y Wi-Fi semanalmente.<br><br>El Modo Firehose también puede funcionar mientras conduce si está conectado a un punto de acceso o tarjeta SIM ilimitada.<br><br><br><b>Preguntas Frecuentes</b><br><br><i>¿Importa cómo o dónde conduzco?</i> No, solo conduzca como lo haría normalmente.<br><br><i>¿Se extraen todos mis segmentos en el Modo Firehose?</i> No, seleccionamos selectivamente un subconjunto de sus segmentos.<br><br><i>¿Qué es un buen adaptador USB-C?</i> Cualquier cargador rápido de teléfono o portátil debería funcionar.<br><br><i>¿Importa qué software ejecuto?</i> Sí, solo el openpilot original (y forks específicos) pueden usarse para el entrenamiento. <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. - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - + <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 - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - + Firehose Mode + Modo manguera contra incendios @@ -546,49 +406,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp MAX - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - InputDialog @@ -603,265 +420,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - LaneChangeSettings - - Back - - - - Auto Lane Change: Delay with Blind Spot - - - - Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. - - - - - LateralPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. - - - - Customize MADS - - - - Customize Lane Change - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - - MadsSettings - - Toggle with Main Cruise - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Unified Engagement Mode (UEM) - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - 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. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - SELECCIONAR - - - No custom model selected! - - - - Driving - - - - Navigation - - - - Vision - - - - Policy - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Formatear Calibración - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - Continuar - - - on Metered - - - - Cancel - Cancelar - - MultiOptionDialog @@ -892,76 +450,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Contraseña incorrecta - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - OffroadAlert @@ -969,12 +457,12 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 + Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 + Conéctese inmediatamente al internet para buscar actualizaciones. Si no se conecta al internet, openpilot 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. + Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. + Conectese al internet para buscar actualizaciones. openpilot no iniciará automáticamente hasta conectarse al internet para buscar actualizaciones. Unable to download updates @@ -990,28 +478,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. 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. + openpilot 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. + openpilot 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 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. + openpilot 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. + openpilot 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 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. + 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. + - 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. + Acknowledge Excessive Actuation + + + + Snooze Update + Posponer Actualización + + + openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. @@ -1033,8 +525,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OnroadAlerts - sunnypilot Unavailable - sunnypilot no disponible + openpilot Unavailable + openpilot no disponible TAKE CONTROL IMMEDIATELY @@ -1087,96 +579,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Cancelar - - ParamControlSP - - Enable - Activar - - - Cancel - Cancelar - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - ELIMINAR - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - Confirmar - - - Cancel - Cancelar - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - PrimeAdWidget @@ -1222,8 +624,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - sunnypilot - sunnypilot + openpilot + openpilot now @@ -1251,47 +653,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - 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. - - SettingsWindow @@ -1320,66 +681,7 @@ Esto puede tardar un minuto. Firehose - - - - - SettingsWindowSP - - × - × - - - Device - Dispositivo - - - Network - Red - - - sunnylink - - - - Toggles - Ajustes - - - Software - Software - - - Trips - - - - Vehicle - - - - Developer - Desarrollador - - - Firehose - - - - Steering - - - - Models - - - - Cruise - - - - Visuals - + Firehose @@ -1441,8 +743,8 @@ Esto puede tardar un minuto. Elija el software a instalar - sunnypilot - sunnypilot + openpilot + openpilot Custom Software @@ -1476,6 +778,18 @@ Esto puede tardar un minuto. Select a language Seleccione un idioma + + WARNING: Custom Software + ADVERTENCIA: Software personalizado + + + Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. + +If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + Tenga cuidado al instalar software de terceros. El software de terceros no ha sido probado por comma y puede causar daños a su dispositivo y/o vehículo. + +Si desea continuar, utilice https://flash.comma.ai para restaurar su dispositivo a un estado de fábrica más tarde. + SetupWidget @@ -1567,33 +881,6 @@ Esto puede tardar un minuto. 5G - - SidebarSP - - DISABLED - - - - OFFLINE - OFFLINE - - - REGIST... - - - - ONLINE - EN LÍNEA - - - ERROR - ERROR - - - SUNNYLINK - - - SoftwarePanel @@ -1669,25 +956,6 @@ Esto puede tardar un minuto. actualizado, último chequeo %1 - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - No branches found for keywords: %1 - - - - Select a branch - Selecione una rama - - SshControl @@ -1734,168 +1002,6 @@ Esto puede tardar un minuto. Habilitar SSH - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - 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 - EMPAREJAR - - - 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. - - - 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 - - - Paired - Emparejado - - - Not Paired - No emparejado - - - THANKS ♥ - GRACIAS ♥ - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - - 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 - - TermsPage @@ -1907,16 +1013,20 @@ Esto puede tardar un minuto. Aceptar - Welcome to sunnypilot - + Welcome to openpilot + Bienvenido a openpilot - 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. - + You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + Debe aceptar los Términos y Condiciones para usar openpilot. Lea los términos más recientes en <span style='color: #465BEA;'>https://comma.ai/terms</span> antes de continuar. TogglesPanel + + Enable openpilot + Activar openpilot + Experimental Mode Modo Experimental @@ -1926,8 +1036,8 @@ Esto puede tardar un minuto. Desactivar con el Acelerador - When enabled, pressing the accelerator pedal will disengage sunnypilot. - Cuando esté activado, presionar el acelerador deshabilitará sunnypilot. + When enabled, pressing the accelerator pedal will disengage openpilot. + Cuando esté activado, presionar el acelerador deshabilitará openpilot. Enable Lane Departure Warnings @@ -1942,7 +1052,7 @@ Esto puede tardar un minuto. Monitoreo Permanente del Conductor - Enable driver monitoring even when sunnypilot is not engaged. + Enable driver monitoring even when openpilot is not engaged. Habilitar el monitoreo del conductor incluso cuando Openpilot no esté activado. @@ -1978,20 +1088,20 @@ Esto puede tardar un minuto. 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. + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot 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, openpilot seguirá más cerca a los autos delante suyo y será más agresivo con el acelerador y el freno. En modo relajado, openpilot 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: + openpilot 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: + openpilot 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. + Let the driving model control the gas and brakes. openpilot 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. openpilot 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 @@ -2006,86 +1116,47 @@ 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. + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + Se puede probar una versión experimental del control longitudinal openpilot, junto con el modo Experimental, en ramas no liberadas. + + + Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. Activar el control longitudinal (fase experimental) para permitir el modo Experimental. - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - 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. - + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Utilice el sistema openpilot para el control de crucero adaptativo y la asistencia al conductor para mantenerse en el carril. Se requiere su atención en todo momento para utilizar esta función. Changing this setting will restart openpilot if the car is powered on. - + Cambiar esta configuración reiniciará Openpilot si el automóvil está encendido. - openpilot longitudinal control may come in a future update. - - - - - Updater - - Update Required - Actualización Requerida + Record and Upload Microphone Audio + Grabar y cargar audio de micrófono - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - Es necesario la actualización del sistema operativo. Conecte su dispositivo al Wi-Fi para una actualización rápida. El tamaño de descarga es de aproximadamente 1GB. - - - Connect to Wi-Fi - Conectarse a la Wi-Fi - - - Install - Instalar - - - Back - Volver - - - Loading... - Cargando... - - - Reboot - Reiniciar - - - Update failed - Actualización fallida + 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. 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..ba1eb93b30 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 @@ -107,34 +103,6 @@ - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - - - ConfirmationDialog @@ -149,8 +117,8 @@ 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. + You must accept the Terms and Conditions in order to use openpilot. + Vous devez accepter les conditions générales pour utiliser openpilot. Back @@ -171,6 +139,18 @@ 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). + + + On this car, openpilot 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. + Sur cette voiture, openpilot utilise par défaut le régulateur de vitesse adaptatif intégré à la voiture plutôt que le contrôle longitudinal d'openpilot. Activez ceci pour passer au contrôle longitudinal openpilot. Il est recommandé d'activer le mode expérimental lors de l'activation du contrôle longitudinal openpilot alpha. + Enable ADB @@ -179,38 +159,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Error Log - - - - VIEW - VOIR - - - View the error log for sunnypilot crashes. - - - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - DevicePanel @@ -263,8 +211,8 @@ Please use caution when using this feature. Only use the blinker when traffic an REVOIR - Review the rules, features, and limitations of sunnypilot - Revoir les règles, fonctionnalités et limitations d'sunnypilot + Review the rules, features, and limitations of openpilot + Revoir les règles, fonctionnalités et limitations d'openpilot Are you sure you want to review the training guide? @@ -302,10 +250,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,121 +303,31 @@ 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. - - - - - DevicePanelSP - - Driver Camera Preview + openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - Training Guide + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - Regulatory - Réglementaire - - - Language + + +Steering lag calibration is %1% complete. - Are you sure you want to review the training guide? - Êtes-vous sûr de vouloir revoir le guide de formation ? - - - Review - Revoir - - - Select a language - Choisir une langue - - - Reboot - Redémarrer - - - Power Off - Éteindre - - - Offroad Mode + + +Steering lag calibration is complete. - Are you sure you want to exit Always Offroad mode? + Steering torque response calibration is %1% complete. - Confirm - Confirmer - - - Are you sure you want to enter Always Offroad mode? - - - - 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. - - - - Reset - Réinitialiser - - - The reset cannot be undone. You have been warned. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles + Steering torque response calibration is complete. @@ -498,7 +352,9 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - 🔥 Firehose Mode 🔥 + openpilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. @@ -510,7 +366,7 @@ Please use caution when using this feature. Only use the blinker when traffic an - 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. + 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 openpilot (and particular forks) are able to be used for training. @@ -521,13 +377,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + <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;'>INACTIVE</span>: connect to an unmetered network + Firehose Mode @@ -546,49 +400,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp MAX - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - InputDialog @@ -603,265 +414,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - LaneChangeSettings - - Back - Retour - - - Auto Lane Change: Delay with Blind Spot - - - - Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. - - - - - LateralPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. - - - - Customize MADS - - - - Customize Lane Change - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - - MadsSettings - - Toggle with Main Cruise - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Unified Engagement Mode (UEM) - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - 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. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - SÉLECTIONNER - - - No custom model selected! - - - - Driving - - - - Navigation - - - - Vision - - - - Policy - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Réinitialiser la calibration - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - Continuer - - - on Metered - - - - Cancel - Annuler - - MultiOptionDialog @@ -892,76 +444,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Mot de passe incorrect - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - OffroadAlert @@ -969,12 +451,12 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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 + Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 + Connectez-vous immédiatement à internet pour vérifier les mises à jour. Si vous ne vous connectez pas à internet, openpilot 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. + Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. + Connectez l'appareil à internet pour vérifier les mises à jour. openpilot ne démarrera pas automatiquement tant qu'il ne se connecte pas à internet pour vérifier les mises à jour. Unable to download updates @@ -990,28 +472,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. 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é. + openpilot 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. + openpilot 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 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. + openpilot 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. + openpilot 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 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. + 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. + - 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. + Acknowledge Excessive Actuation + + + + Snooze Update + Reporter la mise à jour + + + openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. @@ -1033,8 +519,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OnroadAlerts - sunnypilot Unavailable - sunnypilot indisponible + openpilot Unavailable + openpilot indisponible TAKE CONTROL IMMEDIATELY @@ -1087,96 +573,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Annuler - - ParamControlSP - - Enable - Activer - - - Cancel - Annuler - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - SUPPRIMER - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - Confirmer - - - Cancel - Annuler - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - PrimeAdWidget @@ -1222,8 +618,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - sunnypilot - sunnypilot + openpilot + openpilot %n minute(s) ago @@ -1251,47 +647,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. - - SettingsWindow @@ -1323,65 +678,6 @@ Cela peut prendre jusqu'à une minute. - - SettingsWindowSP - - × - × - - - Device - Appareil - - - Network - Réseau - - - sunnylink - - - - Toggles - Options - - - Software - Logiciel - - - Trips - - - - Vehicle - - - - Developer - Dév. - - - Firehose - - - - Steering - - - - Models - - - - Cruise - - - - Visuals - - - Setup @@ -1469,13 +765,23 @@ Cela peut prendre jusqu'à une minute. Choisir le logiciel à installer - sunnypilot - sunnypilot + openpilot + openpilot Custom Software Logiciel personnalisé + + WARNING: Custom Software + + + + Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. + +If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + + SetupWidget @@ -1567,33 +873,6 @@ Cela peut prendre jusqu'à une minute. 5G - - SidebarSP - - DISABLED - - - - OFFLINE - HORS LIGNE - - - REGIST... - - - - ONLINE - EN LIGNE - - - ERROR - ERREUR - - - SUNNYLINK - - - SoftwarePanel @@ -1669,25 +948,6 @@ Cela peut prendre jusqu'à une minute. à jour, dernière vérification %1 - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - No branches found for keywords: %1 - - - - Select a branch - Sélectionner une branche - - SshControl @@ -1734,168 +994,6 @@ Cela peut prendre jusqu'à une minute. Activer SSH - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - 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 - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - ASSOCIER - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - THANKS ♥ - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - TermsPage @@ -1907,16 +1005,20 @@ Cela peut prendre jusqu'à une minute. Accepter - Welcome to sunnypilot + Welcome to openpilot - 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. + You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. TogglesPanel + + Enable openpilot + Activer openpilot + Experimental Mode Mode expérimental @@ -1926,8 +1028,8 @@ Cela peut prendre jusqu'à une minute. 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. + When enabled, pressing the accelerator pedal will disengage openpilot. + Lorsqu'il est activé, appuyer sur la pédale d'accélérateur désengagera openpilot. Enable Lane Departure Warnings @@ -1970,12 +1072,12 @@ Cela peut prendre jusqu'à une minute. 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 : + openpilot 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, openpilot 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. + Let the driving model control the gas and brakes. openpilot 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. openpilot 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 @@ -1986,20 +1088,24 @@ 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. + + + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + Une version alpha du contrôle longitudinal openpilot peut être testée, avec le mode expérimental, sur des branches non publiées. 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. + Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. + Activer le contrôle longitudinal d'openpilot (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. + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot 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, openpilot suivra les véhicules de plus près et sera plus dynamique avec l'accélérateur et le frein. En mode Détendu, openpilot 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. @@ -2010,23 +1116,11 @@ 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. + Enable driver monitoring even when openpilot is not engaged. + Activer la surveillance conducteur lorsque openpilot n'est pas actif. - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - Enable sunnypilot - Activer sunnypilot - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. @@ -2034,43 +1128,12 @@ Cela peut prendre jusqu'à une minute. - openpilot longitudinal control may come in a future update. + Record and Upload Microphone Audio - - - Updater - Update Required - Mise à jour requise - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - Une mise à jour du système d'exploitation est requise. Connectez votre appareil au Wi-Fi pour une mise à jour plus rapide. La taille du téléchargement est d'environ 1 Go. - - - Connect to Wi-Fi - Se connecter au Wi-Fi - - - Install - Installer - - - Back - Retour - - - Loading... - Chargement... - - - Reboot - Redémarrer - - - Update failed - Échec de la mise à jour + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + @@ -2080,11 +1143,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..fea6e51c07 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,55 +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 - - - - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - + 通信制限のあるWi-Fi接続では大容量データをアップロードしません @@ -149,8 +117,8 @@ 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を使用するためには利用規約に同意する必要があります + You must accept the Terms and Conditions in order to use openpilot. + openpilotを使用するためには利用規約に同意する必要があります Back @@ -171,6 +139,18 @@ 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)が無効化されます。 + + + On this car, openpilot 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. + この車では、openpilotは車両内蔵のACC(アダプティブクルーズコントロール)をデフォルトとして使用し、openpilotのアクセル制御は無効化されています。アクセル制御をopenpilotに切り替えるにはこの設定を有効にしてください。また同時にExperimentalモードを推奨します。 + Enable ADB ADBを有効にする @@ -179,38 +159,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB(Android Debug Bridge)により、USBまたはネットワーク経由でデバイスに接続できます。詳細は、https://docs.comma.ai/how-to/connect-to-comma を参照してください。 - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Error Log - - - - VIEW - 確認 - - - View the error log for sunnypilot crashes. - - - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - DevicePanel @@ -259,8 +207,8 @@ Please use caution when using this feature. Only use the blinker when traffic an 確認 - Review the rules, features, and limitations of sunnypilot - sunnypilotのルール、機能、および制限を確認してください + Review the rules, features, and limitations of openpilot + openpilotのルール、機能、および制限を確認してください Are you sure you want to review the training guide? @@ -294,10 +242,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,125 +300,39 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Reset Calibration - + キャリブレーションをリセットするには運転支援を解除して下さい。 - Resetting calibration will restart openpilot if the car is powered on. - - - - - DevicePanelSP - - Driver Camera Preview - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. + openpilotの本体は左右4°以内、上5°下9°以内の角度で取付ける必要があります。 - Training Guide - + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + openpilot は継続的にキャリブレーションを行っており、リセットが必要になることはほとんどありません。キャリブレーションをリセットすると、車の電源が入っている場合はopenpilotが再起動します。 - Regulatory - 規約 + + +Steering lag calibration is %1% complete. + + +ステアリング遅延のキャリブレーションが%1%完了。 - Language - + + +Steering lag calibration is complete. + + +ステアリング遅延のキャリブレーション完了。 - Are you sure you want to review the training guide? - トレーニングガイドを始めてもよろしいですか? + Steering torque response calibration is %1% complete. + ステアリングトルク応答のキャリブレーションが%1%完了。 - Review - 見る - - - Select a language - 言語を選択 - - - Reboot - 再起動 - - - Power Off - パワーオフ - - - Offroad Mode - - - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - 確認 - - - Are you sure you want to enter Always Offroad mode? - - - - 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. - - - - Reset - リセット - - - The reset cannot be undone. You have been warned. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - + Steering torque response calibration is complete. + ステアリングトルク応答のキャリブレーション完了。 @@ -498,37 +356,39 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - 🔥 Firehose Mode 🔥 - 🔥 Firehoseモード 🔥 + openpilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + openpilotは人間であるあなたの運転から学び、AI学習します。 + +Firehoseモードを有効にすると学習データを最大限アップロードし、openpilotの運転モデルを改善することができます。より多くのデータはより大きなモデルとなり、Experimentalモードの精度を向上させます。 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. - + 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 openpilot (and particular forks) are able to be used for training. + 最大の効果を得るためにはデバイスを屋内に持ち込み、大容量のUSB-C充電器とWi-Fiに毎週接続してください。<br><br>Firehoseモードは公衆無線LANや大容量契約のSIMカードに接続していれば、運転中でも動作します。<br><br><br><b>よくある質問(FAQ)</b><br><br><i>運転のやり方や走る場所は重要ですか?</i> いいえ、普段どおりに運転するだけで大丈夫です。<br><br><i>Firehoseモードでは全てのデータがアップロードされますか?</i> いいえ、アップロードするデータを選ぶことができます。<br><br><i>大容量のUSB-C充電器とは何ですか?</i> スマートフォンやノートパソコンを高速に充電できるものを使って下さい。<br><br><i>どのフォークを使うかは重要ですか?</i>はい、トレーニングには公式のopenpilot(および特定のフォーク)のみが使用できます。 <b>%n segment(s)</b> of your driving is in the training dataset so far. - - + + あなたの運転の<b>%nセグメント</b>がこれまでのトレーニングデータに含まれています。 - - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - <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モード + HudRenderer @@ -545,49 +405,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 最大速度 - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - InputDialog @@ -601,265 +418,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - LaneChangeSettings - - Back - 戻る - - - Auto Lane Change: Delay with Blind Spot - - - - Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. - - - - - LateralPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. - - - - Customize MADS - - - - Customize Lane Change - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - - MadsSettings - - Toggle with Main Cruise - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Unified Engagement Mode (UEM) - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - 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. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - 選択 - - - No custom model selected! - - - - Driving - - - - Navigation - - - - Vision - - - - Policy - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - キャリブレーションリセット - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - 続ける - - - on Metered - - - - Cancel - キャンセル - - MultiOptionDialog @@ -890,85 +448,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp パスワードが違います - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - 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] + Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 + インターネットへ接続してアップデートを確認してください。未接続のままではopenpilotを使用できなくなります。あと[%1] - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - インターネットに接続してアップデートを確認してください。接続するまでsunnypilotは使用できません。 + Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. + インターネットに接続してアップデートを確認してください。接続するまでopenpilotは使用できません。 Unable to download updates @@ -984,33 +472,37 @@ 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)が検出されました。このドライブを使用するとデバイスが多大な電力を消費し過熱する可能性があります。 + openpilot 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. + openpilotが車両を識別できませんでした。車が未対応またはECUが認識されていない可能性があります。該当車両のファームウェアバージョンを追加するためにプルリクエストしてください。サポートが必要な場合は discord.comma.ai に参加することができます。 - 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がデバイスの取り付け位置にずれを検出しました。デバイスの固定とマウントがフロントガラスにしっかりと取り付けられていることを確認してください。 + openpilot 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. + openpilotがデバイスの取り付け位置にずれを検出しました。デバイスの固定とマウントがフロントガラスにしっかりと取り付けられていることを確認してください。 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 has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. + openpilotが過剰な%1の作動を検出しました。ソフトウェアの不具合の可能性があります。https://comma.ai/support からサポートへご連絡下さい。 @@ -1031,8 +523,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OnroadAlerts - sunnypilot Unavailable - sunnypilotは使用できません + openpilot Unavailable + openpilotは使用できません TAKE CONTROL IMMEDIATELY @@ -1085,96 +577,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 有効にする - - ParamControlSP - - Enable - 有効にする - - - Cancel - キャンセル - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - 削除 - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - 確認 - - - Cancel - キャンセル - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - PrimeAdWidget @@ -1220,8 +622,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - sunnypilot - sunnypilot + openpilot + openpilot %n minute(s) ago @@ -1246,47 +648,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp たった今 - - 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. - システムリセットの準備が整いました。すべてのデータと設定を消去するには「確認」を押してください。「キャンセル」を押すとブートを再開します。 - - SettingsWindow @@ -1318,65 +679,6 @@ This may take up to a minute. データ学習 - - SettingsWindowSP - - × - × - - - Device - デバイス - - - Network - ネット - - - sunnylink - - - - Toggles - 機能 - - - Software - ソフト - - - Trips - - - - Vehicle - - - - Developer - 開発 - - - Firehose - データ学習 - - - Steering - - - - Models - - - - Cruise - - - - Visuals - - - Setup @@ -1464,13 +766,25 @@ This may take up to a minute. インストールするソフトウェアを選択してください。 - sunnypilot - sunnypilot + openpilot + openpilot Custom Software カスタムソフトウェア + + WARNING: Custom Software + 警告: カスタムソフトウェア + + + Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. + +If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + サードパーティ製ソフトウェアをインストールする際は注意してください。サードパーティ製ソフトウェアはcommaによってテストされておらず、あなたのデバイスや車両に損害を与える可能性があります。 + +続行したい場合は、後でデバイスを工場出荷時の状態に戻すために https://flash.comma.ai を使用してください。 + SetupWidget @@ -1562,33 +876,6 @@ This may take up to a minute. 5G - - SidebarSP - - DISABLED - - - - OFFLINE - オフライン - - - REGIST... - - - - ONLINE - オンライン - - - ERROR - エラー - - - SUNNYLINK - - - SoftwarePanel @@ -1664,25 +951,6 @@ This may take up to a minute. 無効 - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - No branches found for keywords: %1 - - - - Select a branch - ブランチを選択 - - SshControl @@ -1691,7 +959,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 +967,7 @@ This may take up to a minute. Enter your GitHub username - GitHub のユーザー名を入力してください + GitHubのユーザー名を入力してください LOADING @@ -1729,168 +997,6 @@ This may take up to a minute. SSHの有効化 - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - 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 - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - OK - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - THANKS ♥ - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - TermsPage @@ -1902,19 +1008,23 @@ This may take up to a minute. 同意 - Welcome to sunnypilot - + Welcome to openpilot + openpilotへようこそ - 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. - + You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + openpilotを使用するには利用規約に同意する必要があります。続行する前に最新の規約を以下でご確認ください: <span style='color: #465BEA;'>https://comma.ai/terms</span> TogglesPanel + + Enable openpilot + openpilotを有効化 + 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). @@ -1941,20 +1051,20 @@ This may take up to a minute. アクセルを踏むと運転サポートを中断 - When enabled, pressing the accelerator pedal will disengage sunnypilot. - この機能を有効化すると、sunnypilotを利用中にアクセルを踏むとsunnypilotによる運転サポートを中断します。 + When enabled, pressing the accelerator pedal will disengage openpilot. + この機能を有効化すると、openpilotを利用中にアクセルを踏むとopenpilotによる運転サポートを中断します。 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(実験)モードを有効にすると、以下のアグレッシブな開発中の機能を利用する事ができます。 + openpilot 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: + openpilotは標準ではゆっくりとくつろげる運転を提供します。この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の運転ミスに常に備えて注意してください。 + Let the driving model control the gas and brakes. openpilot 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. + openpilotにアクセルとブレーキを任せます。openpilotは赤信号や一時停止サインでの停止を含み、人間と同じように考えて運転を行います。openpilotが運転速度を決定するため、あなたが設定する速度は上限速度になります。この機能は実験段階のため、openpilotの運転ミスに常に備えて注意してください。 New Driving Visualization @@ -1985,16 +1095,20 @@ 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モードと共に非リリースのブランチでテストすることができます。 + openpilot longitudinal control may come in a future update. + openpilotのアクセル制御は将来のアップデートで利用できる可能性があります。 - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - sunnypilotのアクセル制御機能(アルファ)を有効にして、Experimentalモードを許可してください。 + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + openpilotのアルファ版アクセル制御は、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は先行車から距離を取って走行します。サポートされている車両ではステアリングホイールの距離ボタンでこれらのモードを切り替えることができます。 + Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. + openpilotのアクセル制御機能(アルファ)を有効にして、Experimentalモードを許可してください。 + + + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + 標準モードが推奨されます。アグレッシブモードではopenpilotは先行車に近づいて追従し、アクセルとブレーキがより強気になります。リラックスモードでは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,67 +1119,24 @@ This may take up to a minute. 運転者の常時モニタリング - Enable driver monitoring even when sunnypilot is not engaged. - sunnypilotが作動していない場合でも運転者モニタリングを有効にする。 + Enable driver monitoring even when openpilot is not engaged. + openpilotが作動していない場合でも運転者モニタリングを有効にする。 - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - 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. - + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + openpilotシステムを使用してアダプティブクルーズコントロールおよび車線維持支援を行います。システム使用中は常にドライバーが事故を起こさないように注意を払ってください。 Changing this setting will restart openpilot if the car is powered on. - + この設定を変更すると車の電源が入っている場合はopenpilotが再起動します。 - openpilot longitudinal control may come in a future update. - - - - - Updater - - Update Required - アップデートが必要です + Record and Upload Microphone Audio + マイク音声の録音とアップロード - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - OSのアップデートが必要です。Wi-Fiに接続してアップデートする事をお勧めします。ダウンロードサイズは約1GBです。 - - - Connect to Wi-Fi - Wi-Fiに接続 - - - Install - インストール - - - Back - 戻る - - - Loading... - 読み込み中... - - - Reboot - 再起動 - - - Update failed - 更新失敗 + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + 運転中にマイク音声を録音・保存します。音声は comma connect のドライブレコーダー映像に含まれます。 @@ -2075,12 +1146,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..aaefdb92b0 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,55 +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 - - - - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - + 제한된 Wi-Fi 사용 시 대용량 데이터 업로드 방지 @@ -149,8 +117,8 @@ 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을 사용하려면 이용약관에 동의해야 합니다. + You must accept the Terms and Conditions in order to use openpilot. + 오픈파일럿을 사용하려면 이용약관에 동의해야 합니다. Back @@ -169,7 +137,19 @@ 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)기능이 작동하지 않습니다. + + + On this car, openpilot 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. + 이 차량에서 오픈파일럿은 오픈파일럿 가감속 제어 대신 기본적으로 차량의 ACC로 가감속을 제어합니다. 오픈파일럿 가감속 제어로 전환하려면 이 기능을 활성화하세요. 오픈파일럿 가감속 제어 알파 기능을 활성화하는 경우 실험 모드 활성화를 권장합니다. Enable ADB @@ -179,38 +159,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB (안드로이드 디버그 브릿지) USB 또는 네트워크를 통해 장치에 연결할 수 있습니다. 자세한 내용은 https://docs.comma.ai/how-to/connect-to-comma를 참조하세요. - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Error Log - - - - VIEW - 보기 - - - View the error log for sunnypilot crashes. - - - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - DevicePanel @@ -259,8 +207,8 @@ Please use caution when using this feature. Only use the blinker when traffic an 다시보기 - Review the rules, features, and limitations of sunnypilot - sunnypilot의 규칙, 기능 및 제한 다시 확인 + Review the rules, features, and limitations of openpilot + 오픈파일럿의 규칙, 기능 및 제한 다시 확인 Are you sure you want to review the training guide? @@ -294,10 +242,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,125 +300,39 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Reset Calibration - + 캘리브레이션을 재설정하려면 해제하세요 - Resetting calibration will restart openpilot if the car is powered on. - - - - - DevicePanelSP - - Driver Camera Preview - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. + 오픈파일럿 장치는 좌우측으로는 4° 또는 위로는 5° 아래로는 9° 이내에 장착해야합니다. - Training Guide - + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + 오픈파일럿은 지속적으로 갤리브레이션되어 재설정이 거의 필요하지 않습니다. 차량과 연결된 경우 캘리브레이션 재설정이 오픈파일럿을 재시작합니다. - Regulatory - 규제 + + +Steering lag calibration is %1% complete. + + +조향 지연 캘리브레이션이 %1% 진행되었습니다. - Language - + + +Steering lag calibration is complete. + + +조향 지연 캘리브레이션이 완료되었습니다. - Are you sure you want to review the training guide? - 트레이닝 가이드를 다시 확인하시겠습니까? + Steering torque response calibration is %1% complete. + 조향 토크 응답 캘리브레이션이 %1% 진행되었습니다. - Review - 다시보기 - - - Select a language - 언어를 선택하세요 - - - Reboot - 재부팅 - - - Power Off - 전원 끄기 - - - Offroad Mode - - - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - 확인 - - - Are you sure you want to enter Always Offroad mode? - - - - 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. - - - - Reset - 초기화 - - - The reset cannot be undone. You have been warned. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - + Steering torque response calibration is complete. + 조향 토크 응답 캘리브레이션이 완료되었습니다. @@ -498,8 +356,12 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - 🔥 Firehose Mode 🔥 - 🔥 파이어호스 모드 🔥 + openpilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + 오픈파일럿은 여러분과 같은 사람이 운전하는 모습을 보면서 운전하는 법을 배웁니다. + +파이어호스 모드를 사용하면 학습 데이터 업로드를 최대화하여 오픈파일럿의 주행 모델을 개선할 수 있습니다. 더 많은 데이터는 더 큰 모델을 의미하며, 이는 더 나은 실험 모드를 의미합니다. Firehose Mode: ACTIVE @@ -507,27 +369,25 @@ 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>파이어호스 모드에서 제 모든 구간을 가져오나요?.<br><br><i> 아니요, 저희는 여러분의 구간 중 일부를 선별적으로 가져옵니다.<br><br><i>좋은 USB-C 케이블은 무엇인가요?</i> 휴대폰이나 노트북 고속 충전기가 있으면 됩니다.<br><br><i>어떤 소프트웨어를 실행하는지가 중요한가요?</i> 예, 오직 공식 sunnypilot의 특정 포크만 트레이닝에 사용할 수 있습니다. + 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 openpilot (and particular forks) are able to be used for training. + 최대한의 효과를 얻으려면 매주 장치를 실내로 가져와 좋은 USB-C 충전기와 Wi-Fi에 연결하세요.<br><br>파이어호스 모드는 핫스팟 또는 무제한 네트워크에 연결된 경우 주행 중에도 작동할 수 있습니다.<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> 예, 오직 공식 오픈파일럿의 특정 포크만 트레이닝에 사용할 수 있습니다. <b>%n segment(s)</b> of your driving is in the training dataset so far. - - + + <b>%n 구간</b> 의 운전이 지금까지의 학습 데이터셋에 포함되어 있습니다. - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - + <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>: 무제한 네트워크에 연결 하세요 - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - + Firehose Mode + 파이어호스 모드 @@ -545,49 +405,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp MAX - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - InputDialog @@ -601,265 +418,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - LaneChangeSettings - - Back - 뒤로 - - - Auto Lane Change: Delay with Blind Spot - - - - Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. - - - - - LateralPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. - - - - Customize MADS - - - - Customize Lane Change - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - - MadsSettings - - Toggle with Main Cruise - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Unified Engagement Mode (UEM) - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - 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. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - 선택 - - - No custom model selected! - - - - Driving - - - - Navigation - - - - Vision - - - - Policy - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - 캘리브레이션 - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - 계속 - - - on Metered - - - - Cancel - 취소 - - MultiOptionDialog @@ -890,85 +448,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 비밀번호가 틀렸습니다 - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - 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이 활성화되지 않습니다. + Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 + 즉시 인터넷에 연결하여 업데이트를 확인하세요. 인터넷에 연결되어 있지 않으면 %1 이후에는 오픈파일럿이 활성화되지 않습니다. - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - 업데이트를 확인하려면 인터넷에 연결하세요. sunnypilot은 업데이트를 확인하기 위해 인터넷에 연결할 때까지 자동으로 시작되지 않습니다. + Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. + 업데이트 확인을 위해 인터넷 연결이 필요합니다. 오픈파일럿은 업데이트 확인을 위해 인터넷에 연결될 때까지 자동으로 시작되지 않습니다. Unable to download updates @@ -984,33 +472,37 @@ 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 드라이브는 많은 전력을 소비하고 장치를 과열시킬 수 있습니다. + openpilot 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. + 오픈파일럿이 차량을 식별할 수 없습니다. 지원되지 않는 차량이거나 ECU가 인식되지 않습니다. 해당 차량에 맞는 펌웨어 버전을 추가하려면 PR을 제출하세요. 도움이 필요하시면 discord.comma.ai에 참여하세요. - 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 장치의 장착 위치가 변경되었습니다. 장치가 마운트에 완전히 장착되고 마운트가 앞유리에 단단히 고정되었는지 확인하세요. + openpilot 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. + 오픈파일럿 장치의 장착 위치가 변경되었습니다. 장치가 마운트에 완전히 장착되고 마운트가 앞유리에 단단히 고정되었는지 확인하세요. Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - 장치의 온도가 너무 높습니다. 시작 전에 온도를 낮춰주세요. 현재 내부 구성 요소 온도: %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 has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. + 오픈파일럿은 과도한 %1 작동을 감지했습니다. 소프트웨어 버그 때문일 수 있습니다. https://comma.ai/support 에 문의하여 지원받으세요. @@ -1031,7 +523,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OnroadAlerts - sunnypilot Unavailable + openpilot Unavailable 오픈파일럿을 사용할수없습니다 @@ -1085,96 +577,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 활성화 - - ParamControlSP - - Enable - 활성화 - - - Cancel - 취소 - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - 삭제 - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - 확인 - - - Cancel - 취소 - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - PrimeAdWidget @@ -1183,7 +585,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 +601,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 1 year of drive storage - 1년간 드라이브 로그 저장 + 1년간 주행 로그 저장 Remote snapshots @@ -1220,8 +622,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - sunnypilot - sunnypilot + openpilot + 오픈파일럿 %n minute(s) ago @@ -1246,47 +648,6 @@ 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. - 장치를 초기화하는 중... -최대 1분이 소요될 수 있습니다. - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - 시스템 재설정이 시작되었습니다. 모든 콘텐츠와 설정을 지우려면 확인을 누르시고 부팅을 재개하려면 취소를 누르세요. - - SettingsWindow @@ -1318,65 +679,6 @@ This may take up to a minute. 파이어호스 - - SettingsWindowSP - - × - × - - - Device - 장치 - - - Network - 네트워크 - - - sunnylink - - - - Toggles - 토글 - - - Software - 소프트웨어 - - - Trips - - - - Vehicle - - - - Developer - 개발자 - - - Firehose - 파이어호스 - - - Steering - - - - Models - - - - Cruise - - - - Visuals - - - Setup @@ -1464,13 +766,25 @@ This may take up to a minute. 설치할 소프트웨어 선택 - sunnypilot - sunnypilot + openpilot + 오픈파일럿 Custom Software 커스텀 소프트웨어 + + WARNING: Custom Software + 경고: 커스텀 소프트웨어 + + + Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. + +If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + 타사 소프트웨어를 설치할 때는 주의하십시오. 타사 소프트웨어는 comma에 의해 테스트되지 않았으며 장치나 차량에 손상을 줄 수 있습니다. + +진행하려면 https://flash.comma.ai를 사용하여 나중에 장치를 공장 초기화하세요. + SetupWidget @@ -1562,33 +876,6 @@ This may take up to a minute. 5G - - SidebarSP - - DISABLED - - - - OFFLINE - 연결 안됨 - - - REGIST... - - - - ONLINE - 온라인 - - - ERROR - 오류 - - - SUNNYLINK - - - SoftwarePanel @@ -1664,25 +951,6 @@ This may take up to a minute. 업데이트 안함 - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - No branches found for keywords: %1 - - - - Select a branch - 브랜치 선택 - - SshControl @@ -1729,168 +997,6 @@ This may take up to a minute. SSH 사용 - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - 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 - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - 동기화 - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - THANKS ♥ - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - TermsPage @@ -1902,16 +1008,20 @@ This may take up to a minute. 동의 - Welcome to sunnypilot - 오픈 파일럿에 오신 것을 환영합니다. + Welcome to openpilot + 오픈파일럿에 오신 것을 환영합니다. - 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. + You must accept the Terms and Conditions to use openpilot. 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> 에서 최신 약관을 읽은 후 계속하세요. TogglesPanel + + Enable openpilot + 오픈파일럿 사용 + Enable Lane Departure Warnings 차선 이탈 경고 활성화 @@ -1941,20 +1051,20 @@ This may take up to a minute. 가속페달 조작 시 해제 - When enabled, pressing the accelerator pedal will disengage sunnypilot. - 활성화된 경우 가속 페달을 밟으면 sunnypilot이 해제됩니다. + When enabled, pressing the accelerator pedal will disengage openpilot. + 활성화된 경우 가속 페달을 밟으면 오픈파일럿이 해제됩니다. 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>을 활성화합니다. 실험 모드의 기능은 아래와 같습니다: + openpilot 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: + 오픈파일럿은 기본적으로 <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은 신호등과 정지 표지판을 보고 멈추는 것을 포함하여 인간이 운전하는 것처럼 생각하고 주행합니다. 주행 모델이 주행할 속도를 결정하므로 설정된 속도는 최대 주행 속도로만 기능합니다. 이 기능은 알파 수준이므로 사용에 각별히 주의해야 합니다. + Let the driving model control the gas and brakes. openpilot 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 @@ -1962,7 +1072,11 @@ 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로 가감속을 제어하기 때문에 현재 이 차량에서는 실험 모드를 사용할 수 없습니다. + 차량의 ACC가 가감속 제어에 사용되기 때문에, 이 차량에서는 실험 모드를 사용할 수 없습니다. + + + openpilot longitudinal control may come in a future update. + 오픈파일럿 가감속 제어는 향후 업데이트에서 지원될 수 있습니다. Aggressive @@ -1981,20 +1095,20 @@ 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 가감속 제어 알파 버전은 비 릴리즈 브랜치에서 실험 모드와 함께 테스트할 수 있습니다. + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + 오픈파일럿 가감속 제어 알파 버전은 비 릴리즈 브랜치에서 실험 모드와 함께 테스트할 수 있습니다. - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - 실험 모드를 사용하려면 sunnypilot E2E 가감속 제어 (알파) 토글을 활성화하세요. + Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. + 실험 모드를 사용하려면 오픈파일럿 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은 선두 차량으로부터 더 멀리 떨어져 있습니다. 지원되는 차량에서는 스티어링 휠 거리 버튼을 사용하여 이러한 특성을 순환할 수 있습니다. + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + 표준 모드를 권장합니다. 공격적 모드의 오픈파일럿은 선두 차량을 더 가까이 따라가고 가감속제어를 사용하여 더욱 공격적으로 움직입니다. 편안한 모드의 오픈파일럿은 선두 차량으로부터 더 멀리 떨어져 있습니다. 지원되는 차량에서는 차간거리 버튼을 사용하여 이러한 특성을 순환할 수 있습니다. 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,67 +1119,24 @@ This may take up to a minute. 상시 운전자 모니터링 - Enable driver monitoring even when sunnypilot is not engaged. - sunnypilot이 활성화되지 않은 경우에도 드라이버 모니터링을 활성화합니다. + Enable driver monitoring even when openpilot is not engaged. + 오픈파일럿이 활성화되지 않은 경우에도 드라이버 모니터링을 활성화합니다. - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - 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. - + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + ACC 및 차선 유지 지원을 위해 오픈파일럿 시스템을 사용하십시오. 이 기능을 사용하려면 항상주의를 기울여야합니다. Changing this setting will restart openpilot if the car is powered on. - + 이 설정을 변경하면 차량이 재가동된후 오픈파일럿이 시작됩니다. - openpilot longitudinal control may come in a future update. - - - - - Updater - - Update Required - 업데이트 필요 + Record and Upload Microphone Audio + 마이크 오디오 녹음 및 업로드 - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - OS 업데이트가 필요합니다. 장치를 Wi-Fi에 연결하면 가장 빠르게 업데이트할 수 있습니다. 다운로드 크기는 약 1GB입니다. - - - Connect to Wi-Fi - Wi-Fi 연결 - - - Install - 설치 - - - Back - 뒤로 - - - Loading... - 로딩 중... - - - Reboot - 재부팅 - - - Update failed - 업데이트 실패 + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + 운전 중에 마이크 오디오를 녹음하고 저장하십시오. 오디오는 comma connect의 대시캠 비디오에 포함됩니다. @@ -2075,12 +1146,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..976fc711db 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,55 +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 - - - - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - + Previna o envio de grandes volumes de dados em conexões Wi-Fi com franquia de limite de dados @@ -149,8 +117,8 @@ 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. + You must accept the Terms and Conditions in order to use openpilot. + Você precisa aceitar os Termos e Condições para utilizar openpilot. Back @@ -171,6 +139,18 @@ 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). + + + On this car, openpilot 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. + Neste carro, o openpilot tem como padrão o ACC embutido do carro em vez do controle longitudinal do openpilot. Habilite isso para alternar para o controle longitudinal openpilot. Recomenda-se ativar o modo Experimental ao ativar o embrionário controle longitudinal openpilot. + Enable ADB Habilitar ADB @@ -179,38 +159,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB (Android Debug Bridge) permite conectar ao seu dispositivo por meio do USB ou através da rede. Veja https://docs.comma.ai/how-to/connect-to-comma para maiores informações. - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Error Log - - - - VIEW - VER - - - View the error log for sunnypilot crashes. - - - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - DevicePanel @@ -259,8 +207,8 @@ Please use caution when using this feature. Only use the blinker when traffic an REVISAR - Review the rules, features, and limitations of sunnypilot - Revisar regras, aprimoramentos e limitações do sunnypilot + Review the rules, features, and limitations of openpilot + Revisar regras, aprimoramentos e limitações do openpilot Are you sure you want to review the training guide? @@ -294,10 +242,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,125 +300,39 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Reset Calibration - + Desacione para Resetar a Calibração - Resetting calibration will restart openpilot if the car is powered on. - - - - - DevicePanelSP - - Driver Camera Preview - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. + O openpilot exige que o dispositivo seja montado dentro de 4° para a esquerda ou direita e dentro de 5° para cima ou 9° para baixo. - Training Guide - + 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. - Regulatory - Regulatório + + +Steering lag calibration is %1% complete. + + +A calibração do atraso da direção está %1% concluída. - Language - + + +Steering lag calibration is complete. + + +A calibração do atraso da direção foi concluída. - Are you sure you want to review the training guide? - Tem certeza que quer rever o treinamento? + Steering torque response calibration is %1% complete. + A calibração da resposta de torque da direção está %1% concluída. - Review - Revisar - - - Select a language - Selecione o Idioma - - - Reboot - Reiniciar - - - Power Off - Desligar - - - Offroad Mode - - - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - Confirmar - - - Are you sure you want to enter Always Offroad mode? - - - - 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. - - - - Reset - Resetar - - - The reset cannot be undone. You have been warned. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - + Steering torque response calibration is complete. + A calibração da resposta do torque da direção foi concluída. @@ -498,8 +356,12 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - 🔥 Firehose Mode 🔥 - 🔥 Modo Firehose 🔥 + openpilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + O openpilot aprende a dirigir observando humanos, como você, dirigirem. + +O Modo Firehose permite maximizar o envio de dados de treinamento para melhorar os modelos de direção do openpilot. Mais dados significam modelos maiores, o que resulta em um Modo Experimental melhor. Firehose Mode: ACTIVE @@ -510,8 +372,8 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. + 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 openpilot (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 openpilot 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,16 +382,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. - - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - <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 + HudRenderer @@ -546,49 +406,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp LIMITE - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - InputDialog @@ -603,265 +420,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - LaneChangeSettings - - Back - Voltar - - - Auto Lane Change: Delay with Blind Spot - - - - Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. - - - - - LateralPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. - - - - Customize MADS - - - - Customize Lane Change - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - - MadsSettings - - Toggle with Main Cruise - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Unified Engagement Mode (UEM) - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - 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. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - SELECIONE - - - No custom model selected! - - - - Driving - - - - Navigation - - - - Vision - - - - Policy - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Reinicializar Calibragem - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - Continuar - - - on Metered - - - - Cancel - Cancelar - - MultiOptionDialog @@ -892,85 +450,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Senha incorreta - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - 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. + Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot 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 openpilot. - 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. + Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. + Conecte-se à internet para verificar se há atualizações. O openpilot não será iniciado automaticamente até que ele se conecte à internet para verificar se há atualizações. Unable to download updates @@ -986,33 +474,37 @@ 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. + openpilot 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 openpilot 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 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. + openpilot 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 openpilot 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 has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. + O openpilot detectou uma atuação excessiva de %1. Isso pode ser causado por uma falha no software. Por favor, entre em contato com o suporte em https://comma.ai/support. @@ -1033,8 +525,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OnroadAlerts - sunnypilot Unavailable - sunnypilot Indisponível + openpilot Unavailable + openpilot Indisponível TAKE CONTROL IMMEDIATELY @@ -1087,96 +579,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Ativar - - ParamControlSP - - Enable - Ativar - - - Cancel - Cancelar - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - REMOVER - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - Confirmar - - - Cancel - Cancelar - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - PrimeAdWidget @@ -1222,8 +624,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - sunnypilot - sunnypilot + openpilot + openpilot %n minute(s) ago @@ -1251,47 +653,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 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. - - SettingsWindow @@ -1323,65 +684,6 @@ Isso pode levar até um minuto. Firehose - - SettingsWindowSP - - × - × - - - Device - Dispositivo - - - Network - Rede - - - sunnylink - - - - Toggles - Ajustes - - - Software - Software - - - Trips - - - - Vehicle - - - - Developer - Desenvdor - - - Firehose - Firehose - - - Steering - - - - Models - - - - Cruise - - - - Visuals - - - Setup @@ -1469,13 +771,25 @@ Isso pode levar até um minuto. Escolha o Software a ser Instalado - sunnypilot - sunnypilot + openpilot + openpilot Custom Software Software Customizado + + WARNING: Custom Software + AVISO: Software Personalizado + + + Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. + +If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + Tenha cuidado ao instalar software de terceiros. Softwares de terceiros não foram testados pela comma e podem causar danos ao seu dispositivo e/ou veículo. + +Se quiser continuar, use https://flash.comma.ai para restaurar seu dispositivo ao estado de fábrica mais tarde. + SetupWidget @@ -1567,33 +881,6 @@ Isso pode levar até um minuto. 5G - - SidebarSP - - DISABLED - - - - OFFLINE - OFFLINE - - - REGIST... - - - - ONLINE - ONLINE - - - ERROR - ERRO - - - SUNNYLINK - - - SoftwarePanel @@ -1669,25 +956,6 @@ Isso pode levar até um minuto. nunca - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - No branches found for keywords: %1 - - - - Select a branch - Selecione uma branch - - SshControl @@ -1734,168 +1002,6 @@ Isso pode levar até um minuto. Habilitar SSH - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - 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 - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - PAREAR - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - THANKS ♥ - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - TermsPage @@ -1907,16 +1013,20 @@ Isso pode levar até um minuto. Concordo - Welcome to sunnypilot - Bem vindo ao sunnypilot + Welcome to openpilot + Bem vindo ao openpilot - 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. + You must accept the Terms and Conditions to use openpilot. 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 openpilot. Leia os termos mais recentes em <span style='color: #465BEA;'>https://comma.ai/terms</span> antes de continuar. TogglesPanel + + Enable openpilot + Ativar openpilot + Enable Lane Departure Warnings Ativar Avisos de Saída de Faixa @@ -1946,20 +1056,20 @@ Isso pode levar até um minuto. Desacionar com Pedal do Acelerador - When enabled, pressing the accelerator pedal will disengage sunnypilot. - Quando ativado, pressionar o pedal do acelerador desacionará o sunnypilot. + When enabled, pressing the accelerator pedal will disengage openpilot. + Quando ativado, pressionar o pedal do acelerador desacionará o openpilot. 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: + openpilot 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: + openpilot 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. + Let the driving model control the gas and brakes. openpilot 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 openpilot 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 @@ -1969,6 +1079,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 @@ -1986,20 +1100,20 @@ Isso pode levar até um minuto. 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. + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + Uma versão embrionária do controle longitudinal openpilot 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. + Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. + Habilite o controle longitudinal (embrionário) openpilot 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. + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot 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 openpilot seguirá o carro da frente mais de perto e será mais agressivo com a aceleração e frenagem. No modo calmo o openpilot 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. @@ -2010,67 +1124,24 @@ 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. + Enable driver monitoring even when openpilot is not engaged. + Habilite o monitoramento do motorista mesmo quando o openpilot não estiver acionado. - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - 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. - + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Use o sistema openpilot para controle de cruzeiro adaptativo e assistência ao motorista de manutenção de faixa. Sua atenção é necessária o tempo todo para usar esse recurso. 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. - openpilot longitudinal control may come in a future update. - - - - - Updater - - Update Required - Atualização Necessária + Record and Upload Microphone Audio + Gravar e Fazer Upload do Áudio do Microfone - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - Uma atualização do sistema operacional é necessária. Conecte seu dispositivo ao Wi-Fi para a experiência de atualização mais rápida. O tamanho do download é de aproximadamente 1GB. - - - Connect to Wi-Fi - Conecte-se ao Wi-Fi - - - Install - Instalar - - - Back - Voltar - - - Loading... - Carregando... - - - Reboot - Reiniciar - - - Update failed - Falha na atualização + 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. @@ -2080,12 +1151,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..3821b3b806 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 รีบูตและอัปเดต @@ -107,34 +103,6 @@ - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - - - ConfirmationDialog @@ -149,8 +117,8 @@ 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 + You must accept the Terms and Conditions in order to use openpilot. + คุณต้องยอมรับเงื่อนไขและข้อตกลง เพื่อใช้งาน openpilot Back @@ -165,51 +133,31 @@ Please use caution when using this feature. Only use the blinker when traffic an DeveloperPanel Joystick Debug Mode - + โหมดดีบักจอยสติ๊ก Longitudinal Maneuver Mode - - - - Enable ADB - - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Error Log - - - - VIEW - ดู - - - View the error log for sunnypilot crashes. - + โหมดการควบคุมการเร่ง/เบรค 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) จะถูกปิด - 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. - + On this car, openpilot 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. + โดยปกติสำหรับรถคันนี้ openpilot จะควบคุมการเร่ง/เบรคด้วยระบบ ACC จากโรงงาน แทนการควยคุมโดย openpilot เปิดสวิตซ์นี้เพื่อให้ openpilot ควบคุมการเร่ง/เบรค แนะนำให้เปิดโหมดทดลองเมื่อต้องการให้ openpilot ควบคุมการเร่ง/เบรค ซึ่งอยู่ในสถานะ alpha + + + 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 @@ -259,8 +207,8 @@ Please use caution when using this feature. Only use the blinker when traffic an ทบทวน - Review the rules, features, and limitations of sunnypilot - ตรวจสอบกฎ คุณสมบัติ และข้อจำกัดของ sunnypilot + Review the rules, features, and limitations of openpilot + ตรวจสอบกฎ คุณสมบัติ และข้อจำกัดของ openpilot Are you sure you want to review the training guide? @@ -294,10 +242,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,121 +303,31 @@ 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. - - - - - DevicePanelSP - - Driver Camera Preview + openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - Training Guide + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - Regulatory - ระเบียบข้อบังคับ - - - Language + + +Steering lag calibration is %1% complete. - Are you sure you want to review the training guide? - คุณแน่ใจหรือไม่ว่าต้องการทบทวนคู่มือการใช้งาน? - - - Review - ทบทวน - - - Select a language - เลือกภาษา - - - Reboot - รีบูต - - - Power Off - ปิดเครื่อง - - - Offroad Mode + + +Steering lag calibration is complete. - Are you sure you want to exit Always Offroad mode? + Steering torque response calibration is %1% complete. - Confirm - ยืนยัน - - - Are you sure you want to enter Always Offroad mode? - - - - 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. - - - - Reset - รีเซ็ต - - - The reset cannot be undone. You have been warned. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles + Steering torque response calibration is complete. @@ -498,35 +352,37 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - 🔥 Firehose Mode 🔥 - + openpilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + openpilot เรียนรู้วิธีขับรถจากการเฝ้าดูการขับขี่ของมนุษย์เช่นคุณ + +โหมดสายยางดับเพลิงช่วยให้คุณอัปโหลดข้อมูลการฝึกฝนได้มากที่สุด เพื่อนำไปพัฒนาโมเดลการขับขี่ของ openpilot ข้อมูลที่มากขึ้นหมายถึงโมเดลที่ใหญ่ขึ้น และนั่นหมายถึงโหมดทดลองที่ดีขึ้น 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. - + 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 openpilot (and particular forks) are able to be used for training. + เพื่อประสิทธิภาพสูงสุด ควรนำอุปกรณ์เข้ามาข้างใน เชื่อมต่อกับอะแดปเตอร์ USB-C คุณภาพดี และ Wi-Fi สัปดาห์ละครั้ง<br><br>โหมดสายยางดับเพลิงยังสามารถทำงานระหว่างขับรถได้ หากเชื่อมต่อกับฮอตสปอตหรือซิมการ์ดที่มีเน็ตไม่จำกัด<br><br><br><b>คำถามที่พบบ่อย</b><br><br><i>วิธีการขับหรือสถานที่ขับขี่มีผลหรือไม่?</i>ไม่มีผล แค่ขับขี่ตามปกติของคุณ<br><br><i>เซกเมนต์ทั้งหมดของฉันจะถูกดึงข้อมูลในโหมดสายยางดับเพลิงหรือไม่?</i>ไม่ใช่ เราจะเลือกดึงข้อมูลเพียงบางส่วนจากเซกเมนต์ของคุณ<br><br><i>อะแดปเตอร์ USB-C แบบไหนดี?</i>ที่ชาร์จเร็วของโทรศัพท์หรือแล็ปท็อปแบบใดก็ได้ สามารถใช้ได้<br><br><i>ซอฟต์แวร์ที่ใช้มีผลหรือไม่?</i>มีผล เฉพาะ openpilot ตัวหลัก (และ fork เฉพาะบางตัว) เท่านั้น ที่สามารถนำข้อมูลไปใช้ฝึกฝนโมเดลได้ <b>%n segment(s)</b> of your driving is in the training dataset so far. - - + + มีการขับขี่ของคุณ <b>%n เซกเมนต์</b> อยู่ในชุดข้อมูลการฝึกฝนแล้วในขณะนี้ - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - + <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>: เชื่อมต่อกับเครือข่ายที่ไม่จำกัดข้อมูล - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + Firehose Mode @@ -545,49 +401,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp สูงสุด - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - InputDialog @@ -601,265 +414,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - LaneChangeSettings - - Back - ย้อนกลับ - - - Auto Lane Change: Delay with Blind Spot - - - - Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. - - - - - LateralPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. - - - - Customize MADS - - - - Customize Lane Change - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - - MadsSettings - - Toggle with Main Cruise - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Unified Engagement Mode (UEM) - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - 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. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - เลือก - - - No custom model selected! - - - - Driving - - - - Navigation - - - - Vision - - - - Policy - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - รีเซ็ตการคาลิเบรท - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - ดำเนินการต่อ - - - on Metered - - - - Cancel - ยกเลิก - - MultiOptionDialog @@ -890,76 +444,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp รหัสผ่านผิด - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - OffroadAlert @@ -967,12 +451,12 @@ Firehose Mode allows you to maximize your training data uploads to improve openp อุณหภูมิของอุปกรณ์สูงเกินไป ระบบกำลังทำความเย็นก่อนเริ่ม อุณหภูมิของชิ้นส่วนภายในปัจจุบัน: %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 + Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 + กรุณาเชื่อมต่ออินเตอร์เน็ตเพื่อตรวจสอบอัปเดทเดี๋ยวนี้ ถ้าคุณไม่เชื่อมต่ออินเตอร์เน็ต openpilot จะไม่ทำงานในอีก %1 - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - กรุณาเชื่อมต่ออินเตอร์เน็ตเพื่อตรวจสอบอัปเดท sunnypilot จะไม่เริ่มทำงานอัตโนมัติจนกว่าจะได้เชื่อมต่อกับอินเตอร์เน็ตเพื่อตรวจสอบอัปเดท + Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. + กรุณาเชื่อมต่ออินเตอร์เน็ตเพื่อตรวจสอบอัปเดท openpilot จะไม่เริ่มทำงานอัตโนมัติจนกว่าจะได้เชื่อมต่อกับอินเตอร์เน็ตเพื่อตรวจสอบอัปเดท Unable to download updates @@ -988,28 +472,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. - ไม่สามารถลงทะเบียนอุปกรณ์ได้ อุปกรณ์จะไม่สามารถเชื่อมต่อหรืออัปโหลดไปยังเซิร์ฟเวอร์ของ 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 ที่ไม่รองรับ + openpilot 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. + openpilot ไม่สามารถระบุรถยนต์ของคุณได้ ระบบอาจไม่รองรับรถยนต์ของคุณหรือไม่รู้จัก ECU กรุณาส่ง pull request เพื่อเพิ่มรุ่นของเฟิร์มแวร์ให้กับรถยนต์ที่เหมาะสม หากต้องการความช่วยเหลือให้เข้าร่วม discord.comma.ai - 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 + openpilot 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. + openpilot ตรวจพบการเปลี่ยนแปลงของตำแหน่งที่ติดตั้ง กรุณาตรวจสอบว่าได้เลื่อนอุปกรณ์เข้ากับจุดติดตั้งจนสุดแล้ว และจุดติดตั้งได้ยึดติดกับกระจกหน้าอย่างแน่นหนา - 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 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. + - 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. + Acknowledge Excessive Actuation + + + + Snooze Update + เลื่อนการอัปเดต + + + openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. @@ -1031,8 +519,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OnroadAlerts - sunnypilot Unavailable - sunnypilot ไม่สามารถใช้งานได้ + openpilot Unavailable + openpilot ไม่สามารถใช้งานได้ TAKE CONTROL IMMEDIATELY @@ -1044,11 +532,11 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Waiting to start - + รอเริ่มทำงาน System Unresponsive - + ระบบไม่ตอบสนอง @@ -1071,7 +559,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 เพื่อทำการจับคู่ครั้งแรกให้เสร็จสิ้น @@ -1085,96 +573,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp ยกเลิก - - ParamControlSP - - Enable - เปิดใช้งาน - - - Cancel - ยกเลิก - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - ลบ - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - ยืนยัน - - - Cancel - ยกเลิก - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - PrimeAdWidget @@ -1203,7 +601,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Remote snapshots - + ภาพถ่ายระยะไกล @@ -1220,8 +618,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - sunnypilot - sunnypilot + openpilot + openpilot %n minute(s) ago @@ -1246,47 +644,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp ตอนนี้ - - 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. - ระบบถูกรีเซ็ต กดยืนยันเพื่อลบข้อมูลและการตั้งค่าทั้งหมด กดยกเลิกเพื่อบูตต่อ - - SettingsWindow @@ -1311,70 +668,11 @@ This may take up to a minute. Developer - + นักพัฒนา Firehose - - - - - SettingsWindowSP - - × - × - - - Device - อุปกรณ์ - - - Network - เครือข่าย - - - sunnylink - - - - Toggles - ตัวเลือก - - - Software - ซอฟต์แวร์ - - - Trips - - - - Vehicle - - - - Developer - - - - Firehose - - - - Steering - - - - Models - - - - Cruise - - - - Visuals - + สายยางดับเพลิง @@ -1464,13 +762,23 @@ This may take up to a minute. เลือกซอฟต์แวร์ที่จะติดตั้ง - sunnypilot - sunnypilot + openpilot + openpilot Custom Software ซอฟต์แวร์ที่กำหนดเอง + + WARNING: Custom Software + + + + Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. + +If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + + SetupWidget @@ -1562,33 +870,6 @@ This may take up to a minute. 5G - - SidebarSP - - DISABLED - - - - OFFLINE - ออฟไลน์ - - - REGIST... - - - - ONLINE - ออนไลน์ - - - ERROR - เกิดข้อผิดพลาด - - - SUNNYLINK - - - SoftwarePanel @@ -1664,25 +945,6 @@ This may take up to a minute. ล่าสุดแล้ว ตรวจสอบครั้งสุดท้ายเมื่อ %1 - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - No branches found for keywords: %1 - - - - Select a branch - เลือก Branch - - SshControl @@ -1729,168 +991,6 @@ This may take up to a minute. เปิดใช้งาน SSH - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - 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 - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - จับคู่ - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - THANKS ♥ - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - TermsPage @@ -1902,16 +1002,20 @@ This may take up to a minute. ยอมรับ - Welcome to sunnypilot - + Welcome to openpilot + ยินดีต้อนรับสู่ openpilot - 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. - + You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + คุณต้องยอมรับข้อกำหนดและเงื่อนไขเพื่อใช้งาน openpilot อ่านข้อกำหนดล่าสุดได้ที่ <span style='color: #465BEA;'>https://comma.ai/terms</span> ก่อนดำเนินการต่อ TogglesPanel + + Enable openpilot + เปิดใช้งาน openpilot + Enable Lane Departure Warnings เปิดใช้งานการเตือนการออกนอกเลน @@ -1941,20 +1045,20 @@ This may take up to a minute. ยกเลิกระบบช่วยขับเมื่อเหยียบคันเร่ง - When enabled, pressing the accelerator pedal will disengage sunnypilot. - เมื่อเปิดใช้งาน การกดแป้นคันเร่งจะเป็นการยกเลิกระบบช่วยขับโดย sunnypilot + When enabled, pressing the accelerator pedal will disengage openpilot. + เมื่อเปิดใช้งาน การกดแป้นคันเร่งจะเป็นการยกเลิกระบบช่วยขับโดย openpilot 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> ซึ่งยังไม่พร้อมสำหรับโหมดชิล ความสามารถในขั้นพัฒนามีดังนี้: + openpilot 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: + โดยปกติ openpilot จะขับใน<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 จะกำหนดความเร็วในการขับด้วยตัวเอง การตั้งความเร็วจะเป็นเพียงการกำหนดความเร็วสูงสูดเท่านั้น ความสามารถนี้ยังอยู่ในขั้นพัฒนา อาจเกิดข้อผิดพลาดขึ้นได้ + Let the driving model control the gas and brakes. openpilot 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. + ให้ openpilot ควบคุมการเร่ง/เบรค โดย openpilot จะขับอย่างที่มนุษย์คิด รวมถึงการหยุดที่ไฟแดง และป้ายหยุดรถ เนื่องจาก openpilot จะกำหนดความเร็วในการขับด้วยตัวเอง การตั้งความเร็วจะเป็นเพียงการกำหนดความเร็วสูงสูดเท่านั้น ความสามารถนี้ยังอยู่ในขั้นพัฒนา อาจเกิดข้อผิดพลาดขึ้นได้ New Driving Visualization @@ -1964,6 +1068,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 ดุดัน @@ -1981,20 +1089,20 @@ 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 เวอร์ชัน alpha สามารถทดสอบได้พร้อมกับโหมดการทดลอง บน branch ที่กำลังพัฒนา + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + ระบบควบคุมการเร่ง/เบรคโดย openpilot เวอร์ชัน alpha สามารถทดสอบได้พร้อมกับโหมดการทดลอง บน branch ที่กำลังพัฒนา End-to-End Longitudinal Control ควบคุมเร่ง/เบรคแบบ End-to-End - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - เปิดระบบควบคุมการเร่ง/เบรคโดย sunnypilot (alpha) เพื่อเปิดใช้งานโหมดทดลอง + Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. + เปิดระบบควบคุมการเร่ง/เบรคโดย openpilot (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 จะอยู่ห่างจากรถคันหน้ามากขึ้น ในรถรุ่นที่รองรับคุณสามารถเปลี่ยนบุคลิกไปแบบต่าง ๆ โดยใช้ปุ่มปรับระยะห่างบนพวงมาลัย + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + แนะนำให้ใช้แบบมาตรฐาน ในโหมดดุดัน openpilot จะตามรถคันหน้าใกล้ขึ้นและเร่งและเบรคแบบดุดันมากขึ้น ในโหมดผ่อนคลาย 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. @@ -2002,26 +1110,14 @@ This may take up to a minute. Always-On Driver Monitoring - + การเฝ้าระวังผู้ขับขี่ตลอดเวลา - Enable driver monitoring even when sunnypilot is not engaged. - + Enable driver monitoring even when openpilot is not engaged. + เปิดใช้งานการเฝ้าระวังผู้ขับขี่แม้เมื่อ openpilot ไม่ได้เข้าควบคุมอยู่ - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - 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. + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. @@ -2029,58 +1125,27 @@ This may take up to a minute. - openpilot longitudinal control may come in a future update. + Record and Upload Microphone Audio - - - Updater - Update Required - จำเป็นต้องอัปเดต - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - จำเป็นต้องมีการอัปเดตระบบปฏิบัติการ เชื่อมต่ออุปกรณ์ของคุณกับ Wi-Fi เพื่อประสบการณ์การอัปเดตที่เร็วที่สุด ขนาดดาวน์โหลดประมาณ 1GB - - - Connect to Wi-Fi - เชื่อมต่อกับ Wi-Fi - - - Install - ติดตั้ง - - - Back - ย้อนกลับ - - - Loading... - กำลังโหลด... - - - Reboot - รีบูต - - - Update failed - การอัปเดตล้มเหลว + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + 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..6cafb1dffc 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 @@ -107,34 +103,6 @@ - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - - - ConfirmationDialog @@ -149,7 +117,7 @@ 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. + You must accept the Terms and Conditions in order to use openpilot. Openpilotu kullanmak için Kullanıcı Koşullarını kabul etmelisiniz. @@ -171,34 +139,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode - - Enable ADB - - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Error Log - - - - VIEW - BAK - - - View the error log for sunnypilot crashes. - - openpilot Longitudinal Control (Alpha) @@ -208,7 +148,15 @@ 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. + On this car, openpilot 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. + + + + Enable ADB + + + + ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. @@ -259,8 +207,8 @@ Please use caution when using this feature. Only use the blinker when traffic an 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. + Review the rules, features, and limitations of openpilot + openpilot sisteminin kurallarını ve sınırlamalarını gözden geçirin. Are you sure you want to review the training guide? @@ -294,10 +242,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,121 +303,31 @@ 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. - - - - - DevicePanelSP - - Driver Camera Preview + openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - Training Guide + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - Regulatory - Mevzuat - - - Language + + +Steering lag calibration is %1% complete. - Are you sure you want to review the training guide? - Eğitim kılavuzunu incelemek istediğinizden emin misiniz? - - - Review + + +Steering lag calibration is complete. - Select a language - Dil seçin - - - Reboot - Yeniden başlat - - - Power Off - Sistemi kapat - - - Offroad Mode + Steering torque response calibration is %1% complete. - Are you sure you want to exit Always Offroad mode? - - - - Confirm - Onayla - - - Are you sure you want to enter Always Offroad mode? - - - - 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. - - - - Reset - - - - The reset cannot be undone. You have been warned. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles + Steering torque response calibration is complete. @@ -498,7 +352,9 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - 🔥 Firehose Mode 🔥 + openpilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. @@ -510,7 +366,7 @@ Please use caution when using this feature. Only use the blinker when traffic an - 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. + 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 openpilot (and particular forks) are able to be used for training. @@ -520,13 +376,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + <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;'>INACTIVE</span>: connect to an unmetered network + Firehose Mode @@ -545,49 +399,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp MAX - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - InputDialog @@ -601,265 +412,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - LaneChangeSettings - - Back - - - - Auto Lane Change: Delay with Blind Spot - - - - Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. - - - - - LateralPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. - - - - Customize MADS - - - - Customize Lane Change - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - - MadsSettings - - Toggle with Main Cruise - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Unified Engagement Mode (UEM) - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - 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. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - - - - No custom model selected! - - - - Driving - - - - Navigation - - - - Vision - - - - Policy - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Kalibrasyonu sıfırla - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - Devam et - - - on Metered - - - - Cancel - - - MultiOptionDialog @@ -890,76 +442,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Yalnış parola - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - OffroadAlert @@ -967,11 +449,11 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot 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. + Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. @@ -987,28 +469,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. + openpilot 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 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. + openpilot 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 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. + 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. - 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. + Acknowledge Excessive Actuation + + + + Snooze Update + Güncellemeyi sessize al + + + openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. @@ -1030,7 +516,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OnroadAlerts - sunnypilot Unavailable + openpilot Unavailable @@ -1084,96 +570,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - ParamControlSP - - Enable - - - - Cancel - - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - KALDIR - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - Onayla - - - Cancel - - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - PrimeAdWidget @@ -1219,8 +615,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - sunnypilot - sunnypilot + openpilot + openpilot %n minute(s) ago @@ -1245,46 +641,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - 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. - - - SettingsWindow @@ -1316,65 +672,6 @@ This may take up to a minute. - - SettingsWindowSP - - × - x - - - Device - Cihaz - - - Network - Ağ - - - sunnylink - - - - Toggles - Değiştirme - - - Software - Yazılım - - - Trips - - - - Vehicle - - - - Developer - - - - Firehose - - - - Steering - - - - Models - - - - Cruise - - - - Visuals - - - Setup @@ -1462,13 +759,23 @@ This may take up to a minute. - sunnypilot - sunnypilot + openpilot + openpilot Custom Software + + WARNING: Custom Software + + + + Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. + +If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + + SetupWidget @@ -1560,33 +867,6 @@ This may take up to a minute. 5G - - SidebarSP - - DISABLED - - - - OFFLINE - ÇEVRİMDIŞI - - - REGIST... - - - - ONLINE - ÇEVRİMİÇİ - - - ERROR - HATA - - - SUNNYLINK - - - SoftwarePanel @@ -1662,25 +942,6 @@ This may take up to a minute. - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - No branches found for keywords: %1 - - - - Select a branch - - - SshControl @@ -1727,168 +988,6 @@ This may take up to a minute. SSH aç - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - 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 - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - THANKS ♥ - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - TermsPage @@ -1900,16 +999,20 @@ This may take up to a minute. Kabul et - Welcome to sunnypilot + Welcome to openpilot - 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. + You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. TogglesPanel + + Enable openpilot + openpilot'u aktifleştir + Enable Lane Departure Warnings Şerit ihlali uyarı alın @@ -1935,8 +1038,8 @@ This may take up to a minute. 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. + When enabled, pressing the accelerator pedal will disengage openpilot. + Aktifleştirilirse eğer gaz pedalına basınca openpilot devre dışı kalır. Experimental Mode @@ -1963,7 +1066,7 @@ This may take up to a minute. - 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: + openpilot 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: @@ -1971,7 +1074,7 @@ This may take up to a minute. - 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. + Let the driving model control the gas and brakes. openpilot 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. @@ -1983,15 +1086,19 @@ 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. + openpilot longitudinal control may come in a future update. - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - 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. + Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. + + + + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. @@ -2003,23 +1110,11 @@ This may take up to a minute. - Enable driver monitoring even when sunnypilot is not engaged. + Enable driver monitoring even when openpilot is not engaged. - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - Enable sunnypilot - sunnypilot'u aktifleştir - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. @@ -2027,43 +1122,12 @@ This may take up to a minute. - openpilot longitudinal control may come in a future update. + Record and Upload Microphone Audio - - - Updater - Update Required - Güncelleme yapılması gerekli - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - İşletim sistemi güncellemesi gerekmektedir. Lütfen Cihazı daha hızlı günceleyebilmesi için bir sonraki başlatılışında çekilir. Wi-Fi ağına bağlayın. Dosyanın boyutu yaklaşık 1GB dır. - - - Connect to Wi-Fi - Wi-Fi ağına bağlan - - - Install - Yükle - - - Back - Geri - - - Loading... - Yükleniyor... - - - Reboot - Yeniden başlat - - - Update failed - Güncelleme başarız oldu + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + @@ -2073,11 +1137,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..5e5b9da311 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,55 +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 - - - - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - + 在按流量计费的 WLAN 网络上,防止上传大数据 @@ -149,8 +117,8 @@ 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。 + You must accept the Terms and Conditions in order to use openpilot. + 您必须接受条款和条件以使用openpilot。 Back @@ -171,6 +139,18 @@ 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)功能。 + + + On this car, openpilot 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. + 在这辆车上,openpilot 默认使用车辆内建的主动巡航控制(ACC),而非 openpilot 的纵向控制。启用此项功能可切换至 openpilot 的纵向控制。当启用 openpilot 纵向控制 Alpha 版本时,建议同时启用实验性模式(Experimental mode)。 + Enable ADB 启用 ADB @@ -179,38 +159,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB(Android调试桥接)允许通过USB或网络连接到您的设备。更多信息请参见 [https://docs.comma.ai/how-to/connect-to-comma](https://docs.comma.ai/how-to/connect-to-comma)。 - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Error Log - - - - VIEW - 查看 - - - View the error log for sunnypilot crashes. - - - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - DevicePanel @@ -259,8 +207,8 @@ Please use caution when using this feature. Only use the blinker when traffic an 查看 - Review the rules, features, and limitations of sunnypilot - 查看 sunnypilot 的使用规则,以及其功能和限制 + Review the rules, features, and limitations of openpilot + 查看 openpilot 的使用规则,以及其功能和限制 Are you sure you want to review the training guide? @@ -294,10 +242,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 +268,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 +276,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Power Off - 取消sunnypilot以关机 + 取消openpilot以关机 Reset @@ -356,125 +300,39 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Reset Calibration - + 解除以重置校准 - Resetting calibration will restart openpilot if the car is powered on. - - - - - DevicePanelSP - - Driver Camera Preview - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. + openpilot 要求设备的安装角度:左右偏移需在 4° 以内,上下俯仰角度需在向上 5° 至向下 9° 的范围内。 - Training Guide - + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + openpilot 会持续进行校准,因此很少需要重置。如果车辆电源已开启,重置校准会重新启动 openpilot。 - Regulatory - 监管信息 + + +Steering lag calibration is %1% complete. + + +转向延迟校准已完成 %1%。 - Language - + + +Steering lag calibration is complete. + + +转向延迟校准已完成。 - Are you sure you want to review the training guide? - 您确定要查看新手指南吗? + Steering torque response calibration is %1% complete. + 转向扭矩响应校准已完成 %1%。 - Review - 预览 - - - Select a language - 选择语言 - - - Reboot - 重启 - - - Power Off - 关机 - - - Offroad Mode - - - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - 确认 - - - Are you sure you want to enter Always Offroad mode? - - - - 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. - - - - Reset - 重置 - - - The reset cannot be undone. You have been warned. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - + Steering torque response calibration is complete. + 转向扭矩响应校准已完成。 @@ -498,20 +356,24 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - 🔥 Firehose Mode 🔥 - 🔥 训练数据上传模式 🔥 + openpilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + openpilot 通过观察人类驾驶(包括您)来学习如何驾驶。 + +“Firehose 模式”允许您最大化上传训练数据,以改进 openpilot 的驾驶模型。更多数据意味着更强大的模型,也就意味着更优秀的“实验模式”。 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(以及特定的分支)可以用于训练。 + 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 openpilot (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>有的,只有官方 openpilot(以及特定的分支)可以用于训练。 <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -519,16 +381,14 @@ Please use caution when using this feature. Only use the blinker when traffic an <b>目前已有 %n 段</b> 您的驾驶数据被纳入训练数据集。 - - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - <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 模式 + HudRenderer @@ -545,49 +405,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 最高定速 - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - InputDialog @@ -601,265 +418,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - LaneChangeSettings - - Back - 返回 - - - Auto Lane Change: Delay with Blind Spot - - - - Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. - - - - - LateralPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. - - - - Customize MADS - - - - Customize Lane Change - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - - MadsSettings - - Toggle with Main Cruise - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Unified Engagement Mode (UEM) - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - 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. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - 选择 - - - No custom model selected! - - - - Driving - - - - Navigation - - - - Vision - - - - Policy - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - 重置设备校准 - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - 继续 - - - on Metered - - - - Cancel - 取消 - - MultiOptionDialog @@ -890,85 +448,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 密码错误 - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - 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 后便无法使用 + Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 + 请立即连接网络检查更新。如果不连接网络,openpilot 将在 %1 后便无法使用 - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - 请连接至互联网以检查更新。在连接至互联网并完成更新检查之前,sunnypilot 将不会自动启动。 + Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. + 请连接至互联网以检查更新。在连接至互联网并完成更新检查之前,openpilot 将不会自动启动。 Unable to download updates @@ -984,32 +472,36 @@ 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 固态硬盘可能会消耗更多电力并更易过热。 + openpilot 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. + openpilot 无法识别您的车辆。您的车辆可能未被支持,或是其电控单元 (ECU) 未被识别。请提交一个 Pull Request 为您的车辆添加正确的固件版本。需要帮助吗?请加入 discord.comma.ai。 - 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 检测到设备的安装位置发生变化。请确保设备完全安装在支架上,并确保支架牢固地固定在挡风玻璃上。 + openpilot 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. + openpilot 检测到设备的安装位置发生变化。请确保设备完全安装在支架上,并确保支架牢固地固定在挡风玻璃上。 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 has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. @@ -1031,8 +523,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OnroadAlerts - sunnypilot Unavailable - 无法使用 sunnypilot + openpilot Unavailable + 无法使用 openpilot TAKE CONTROL IMMEDIATELY @@ -1085,96 +577,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 启用 - - ParamControlSP - - Enable - 启用 - - - Cancel - 取消 - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - 删除 - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - 确认 - - - Cancel - 取消 - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - PrimeAdWidget @@ -1220,8 +622,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - sunnypilot - sunnypilot + openpilot + openpilot %n minute(s) ago @@ -1246,47 +648,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 现在 - - 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. - 系统重置已触发。按下“确认”以清除所有内容和设置,按下“取消”以继续启动。 - - SettingsWindow @@ -1315,66 +676,7 @@ This may take up to a minute. Firehose - 训练上传 - - - - SettingsWindowSP - - × - × - - - Device - 设备 - - - Network - 网络 - - - sunnylink - - - - Toggles - 设定 - - - Software - 软件 - - - Trips - - - - Vehicle - - - - Developer - 开发人员 - - - Firehose - 训练上传 - - - Steering - - - - Models - - - - Cruise - - - - Visuals - + Firehose @@ -1464,13 +766,25 @@ This may take up to a minute. 选择要安装的软件 - sunnypilot - sunnypilot + openpilot + openpilot Custom Software 定制软件 + + WARNING: Custom Software + 警告:自定义软件 + + + Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. + +If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + 请谨慎安装第三方软件。第三方软件未经 comma 测试,可能会损害您的设备和车辆。 + +如果您决定继续,后续可通过 https://flash.comma.ai 将设备恢复到出厂状态。 + SetupWidget @@ -1562,33 +876,6 @@ This may take up to a minute. 5G - - SidebarSP - - DISABLED - - - - OFFLINE - 离线 - - - REGIST... - - - - ONLINE - 在线 - - - ERROR - 连接出错 - - - SUNNYLINK - - - SoftwarePanel @@ -1664,25 +951,6 @@ This may take up to a minute. 从未更新 - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - No branches found for keywords: %1 - - - - Select a branch - 选择分支 - - SshControl @@ -1729,168 +997,6 @@ This may take up to a minute. 启用SSH - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - 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 - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - 配对 - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - THANKS ♥ - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - TermsPage @@ -1902,16 +1008,20 @@ This may take up to a minute. 同意 - Welcome to sunnypilot - 欢迎使用 sunnypilot + Welcome to openpilot + 欢迎使用 openpilot - 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>。 + You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + 您必须接受《条款与条件》才能使用 openpilot。在继续之前,请先阅读最新条款:<span style='color: #465BEA;'>https://comma.ai/terms</span>。 TogglesPanel + + Enable openpilot + 启用openpilot + Enable Lane Departure Warnings 启用车道偏离警告 @@ -1941,20 +1051,20 @@ This may take up to a minute. 踩油门时取消控制 - When enabled, pressing the accelerator pedal will disengage sunnypilot. - 启用后,踩下油门踏板将取消sunnypilot。 + When enabled, pressing the accelerator pedal will disengage openpilot. + 启用后,踩下油门踏板将取消openpilot。 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>。试验性功能包括: + openpilot 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: + openpilot 默认 <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将模仿人类驾驶车辆,包括在红灯和停车让行标识前停车。鉴于驾驶模型确定行驶车速,所设定的车速仅作为上限。此功能尚处于早期测试状态,有可能会出现操作错误。 + Let the driving model control the gas and brakes. openpilot 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. + 允许驾驶模型控制加速和制动,openpilot将模仿人类驾驶车辆,包括在红灯和停车让行标识前停车。鉴于驾驶模型确定行驶车速,所设定的车速仅作为上限。此功能尚处于早期测试状态,有可能会出现操作错误。 New Driving Visualization @@ -1964,6 +1074,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 积极 @@ -1981,20 +1095,20 @@ 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. - 在正式(release)版本以外的分支上,可以测试 sunnypilot 纵向控制的 Alpha 版本以及实验模式。 + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + 在正式(release)版本以外的分支上,可以测试 openpilot 纵向控制的 Alpha 版本以及实验模式。 - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - 启用 sunnypilot 纵向控制(alpha)开关以允许实验模式。 + Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. + 启用 openpilot 纵向控制(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 会与前方车辆保持更远距离。在支持的车型上,你可以使用方向盘上的距离按钮来循环切换这些驾驶风格。 + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + 推荐使用标准模式。在积极模式下,openpilot 会更靠近前方车辆,并在油门和刹车方面更加激进。在放松模式下,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,67 +1119,24 @@ This may take up to a minute. 驾驶员监控常开 - Enable driver monitoring even when sunnypilot is not engaged. - 即使在sunnypilot未激活时也启用驾驶员监控。 + Enable driver monitoring even when openpilot is not engaged. + 即使在openpilot未激活时也启用驾驶员监控。 - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - 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. - + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + openpilot 系统提供“自适应巡航”和“车道保持”驾驶辅助功能。使用此功能时,您需要时刻保持专注。 Changing this setting will restart openpilot if the car is powered on. - + 如果车辆已通电,更改此设置将会重新启动 openpilot。 - openpilot longitudinal control may come in a future update. - - - - - Updater - - Update Required - 需要更新 + Record and Upload Microphone Audio + 录制并上传麦克风音频 - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - 操作系统需要更新。请将您的设备连接到WiFi以获取更快的更新体验。下载大小约为1GB。 - - - Connect to Wi-Fi - 连接到WiFi - - - Install - 安装 - - - Back - 返回 - - - Loading... - 正在加载…… - - - Reboot - 重启 - - - Update failed - 更新失败 + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + 在驾驶时录制并存储麦克风音频。该音频将会包含在 comma connect 的行车记录仪视频中。 @@ -2075,12 +1146,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..630dbf2a08 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,55 +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 - - - - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - + 在使用計費 Wi-Fi 網路時,防止上傳大量數據 @@ -149,8 +117,8 @@ 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。 + You must accept the Terms and Conditions in order to use openpilot. + 您必須先接受條款和條件才能使用 openpilot。 Back @@ -171,6 +139,18 @@ 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)功能。 + + + On this car, openpilot 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. + 在這輛車上,openpilot 預設使用車輛內建的主動巡航控制(ACC),而非 openpilot 的縱向控制。啟用此項功能可切換至 openpilot 的縱向控制。當啟用 openpilot 縱向控制 Alpha 版本時,建議同時啟用實驗性模式(Experimental mode)。 + Enable ADB 啟用 ADB @@ -179,38 +159,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB(Android 調試橋接)允許通過 USB 或網絡連接到您的設備。更多信息請參見 [https://docs.comma.ai/how-to/connect-to-comma](https://docs.comma.ai/how-to/connect-to-comma)。 - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Error Log - - - - VIEW - 觀看 - - - View the error log for sunnypilot crashes. - - - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - DevicePanel @@ -259,8 +207,8 @@ Please use caution when using this feature. Only use the blinker when traffic an 觀看 - Review the rules, features, and limitations of sunnypilot - 觀看 sunnypilot 的使用規則、功能和限制 + Review the rules, features, and limitations of openpilot + 觀看 openpilot 的使用規則、功能和限制 Are you sure you want to review the training guide? @@ -294,10 +242,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,125 +300,39 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Reset Calibration - + 解除以重設校準 - Resetting calibration will restart openpilot if the car is powered on. - - - - - DevicePanelSP - - Driver Camera Preview - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. + openpilot 要求裝置的安裝角度,左右偏移須在 4° 以內,上下角度則須介於仰角 5° 至俯角 9° 之間。 - Training Guide - + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + openpilot 會持續進行校準,因此很少需要重設。若車輛電源開啟,重設校準將會重新啟動 openpilot。 - Regulatory - 法規/監管 + + +Steering lag calibration is %1% complete. + + +轉向延遲校準已完成 %1%。 - Language - + + +Steering lag calibration is complete. + + +轉向延遲校準已完成。 - Are you sure you want to review the training guide? - 您確定要觀看使用教學嗎? + Steering torque response calibration is %1% complete. + 轉向扭矩反應校準已完成 %1%。 - Review - 回顧 - - - Select a language - 選擇語言 - - - Reboot - 重新啟動 - - - Power Off - 關機 - - - Offroad Mode - - - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - 確認 - - - Are you sure you want to enter Always Offroad mode? - - - - 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. - - - - Reset - 重設 - - - The reset cannot be undone. You have been warned. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - + Steering torque response calibration is complete. + 轉向扭矩反應校準已完成。 @@ -498,20 +356,24 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - 🔥 Firehose Mode 🔥 - 🔥 訓練資料上傳模式 🔥 + openpilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + openpilot 透過觀察人類駕駛(包括您)來學習如何駕駛。 + +「Firehose 模式」可讓您最大化上傳訓練數據,以改進 openpilot 的駕駛模型。更多數據代表更強大的模型,也就意味著更優秀的「實驗模式」。 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(以及特定的分支)可以用於訓練。 + 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 openpilot (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>Firehose 模式會上傳所有的駕駛片段嗎?</i>不會,我們會選擇性地上傳部分片段。<br><br><i>什麼是好的 USB-C 充電器?</i>任何快速手機或筆電充電器都應該適用。<br><br><i>我使用的軟體版本有影響嗎?</i>有的,只有官方 openpilot(以及特定的分支)可以用於訓練。 <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -520,14 +382,12 @@ Please use caution when using this feature. Only use the blinker when traffic an - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - + <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>:請連接到不按流量計費的網絡 - <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 模式 @@ -545,49 +405,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 最高 - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - InputDialog @@ -601,265 +418,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - LaneChangeSettings - - Back - 回上頁 - - - Auto Lane Change: Delay with Blind Spot - - - - Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. - - - - - LateralPanel - - Modular Assistive Driving System (MADS) - - - - Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. - - - - Customize MADS - - - - Customize Lane Change - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - - MadsSettings - - Toggle with Main Cruise - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Unified Engagement Mode (UEM) - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - 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. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - 選取 - - - No custom model selected! - - - - Driving - - - - Navigation - - - - Vision - - - - Policy - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - 重設校準 - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - 繼續 - - - on Metered - - - - Cancel - 取消 - - MultiOptionDialog @@ -890,85 +448,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 密碼錯誤 - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - 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 後便無法使用 + Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 + 請立即連接網路檢查更新。如果不連接網路,openpilot 將在 %1 後便無法使用 - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - 請連接至網際網路以檢查更新。在連接至網際網路並完成更新檢查之前,sunnypilot 將不會自動啟動。 + Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. + 請連接至網際網路以檢查更新。在連接至網際網路並完成更新檢查之前,openpilot 將不會自動啟動。 Unable to download updates @@ -984,32 +472,36 @@ 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 固態硬碟可能會消耗更多電力並更易過熱。 + openpilot 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. + openpilot 無法識別您的車輛。您的車輛可能未被支援,或是其電控單元 (ECU) 未被識別。請提交一個 Pull Request 為您的車輛添加正確的韌體版本。需要幫助嗎?請加入 discord.comma.ai 。 - 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 偵測到裝置的安裝位置發生變化。請確保裝置完全安裝在支架上,並確保支架牢固地固定在擋風玻璃上。 + openpilot 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. + openpilot 偵測到裝置的安裝位置發生變化。請確保裝置完全安裝在支架上,並確保支架牢固地固定在擋風玻璃上。 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 has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. @@ -1031,8 +523,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OnroadAlerts - sunnypilot Unavailable - 無法使用 sunnypilot + openpilot Unavailable + 無法使用 openpilot TAKE CONTROL IMMEDIATELY @@ -1085,96 +577,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 啟用 - - ParamControlSP - - Enable - 啟用 - - - Cancel - 取消 - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - 移除 - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - 確認 - - - Cancel - 取消 - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - PrimeAdWidget @@ -1220,8 +622,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - sunnypilot - sunnypilot + openpilot + openpilot %n minute(s) ago @@ -1246,47 +648,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 現在 - - 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. - 系統重設已啟動。按下「確認」以清除所有內容和設定,或按下「取消」以繼續開機。 - - SettingsWindow @@ -1315,66 +676,7 @@ This may take up to a minute. Firehose - 訓練上傳 - - - - SettingsWindowSP - - × - × - - - Device - 裝置 - - - Network - 網路 - - - sunnylink - - - - Toggles - 設定 - - - Software - 軟體 - - - Trips - - - - Vehicle - - - - Developer - 開發人員 - - - Firehose - 訓練上傳 - - - Steering - - - - Models - - - - Cruise - - - - Visuals - + Firehose @@ -1464,13 +766,25 @@ This may take up to a minute. 選擇要安裝的軟體 - sunnypilot - sunnypilot + openpilot + openpilot Custom Software 自訂軟體 + + WARNING: Custom Software + 警告:自訂軟體 + + + Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle. + +If you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later. + 請謹慎安裝第三方軟體。第三方軟體未經 comma 測試,可能會損壞您的裝置及車輛。 + +若您仍要繼續,日後可使用 https://flash.comma.ai 將您的裝置恢復至出廠狀態。 + SetupWidget @@ -1562,33 +876,6 @@ This may take up to a minute. 5G - - SidebarSP - - DISABLED - - - - OFFLINE - 已離線 - - - REGIST... - - - - ONLINE - 已連線 - - - ERROR - 錯誤 - - - SUNNYLINK - - - SoftwarePanel @@ -1664,25 +951,6 @@ This may take up to a minute. 從未更新 - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - No branches found for keywords: %1 - - - - Select a branch - 選取一個分支 - - SshControl @@ -1729,168 +997,6 @@ This may take up to a minute. 啟用 SSH 服務 - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - 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 - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - 配對 - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - THANKS ♥ - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - TermsPage @@ -1902,16 +1008,20 @@ This may take up to a minute. 接受 - Welcome to sunnypilot - 歡迎使用 sunnypilot + Welcome to openpilot + 歡迎使用 openpilot - 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>。 + You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + 您必須接受《條款與條件》才能使用 openpilot。在繼續之前,請先閱讀最新條款:<span style='color: #465BEA;'>https://comma.ai/terms</span>。 TogglesPanel + + Enable openpilot + 啟用 openpilot + Enable Lane Departure Warnings 啟用車道偏離警告 @@ -1941,20 +1051,20 @@ This may take up to a minute. 油門取消控車 - When enabled, pressing the accelerator pedal will disengage sunnypilot. - 啟用後,踩踏油門將會取消 sunnypilot 控制。 + When enabled, pressing the accelerator pedal will disengage openpilot. + 啟用後,踩踏油門將會取消 openpilot 控制。 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>。實驗功能如下: + openpilot 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: + openpilot 預設以 <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將會模擬人類的駕駛行為,包含在看見紅燈及停止標示時停車。由於車速將由駕駛模型決定,因此您設定的時速將成為速度上限。本功能仍在早期實驗階段,請預期模型有犯錯的可能性。 + Let the driving model control the gas and brakes. openpilot 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. + 讓駕駛模型來控制油門及煞車。openpilot將會模擬人類的駕駛行為,包含在看見紅燈及停止標示時停車。由於車速將由駕駛模型決定,因此您設定的時速將成為速度上限。本功能仍在早期實驗階段,請預期模型有犯錯的可能性。 New Driving Visualization @@ -1964,6 +1074,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 積極 @@ -1981,20 +1095,20 @@ 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. - 在正式 (release) 版以外的分支上可以測試 sunnypilot 縱向控制的 Alpha 版本以及實驗模式。 + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + 在正式 (release) 版以外的分支上可以測試 openpilot 縱向控制的 Alpha 版本以及實驗模式。 - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - 啟用 sunnypilot 縱向控制(alpha)切換以允許實驗模式。 + Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. + 啟用 openpilot 縱向控制(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 會與前車保持較遠距離。對於支援的汽車,您可以使用方向盤上的距離按鈕來切換這些駕駛風格。 + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + 建議使用標準模式。在積極模式下,openpilot 會更接近前車並更積極地使用油門和剎車。在輕鬆模式下,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,67 +1119,24 @@ This may take up to a minute. 駕駛監控常開 - Enable driver monitoring even when sunnypilot is not engaged. - 即使在sunnypilot未激活時也啟用駕駛監控。 + Enable driver monitoring even when openpilot is not engaged. + 即使在openpilot未激活時也啟用駕駛監控。 - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - 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. - + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + openpilot 系統提供「主動式巡航」與「車道維持」等駕駛輔助功能。使用這些功能時,您必須隨時保持專注。 Changing this setting will restart openpilot if the car is powered on. - + 若車輛電源為開啟狀態,變更此設定將會重新啟動 openpilot。 - openpilot longitudinal control may come in a future update. - - - - - Updater - - Update Required - 系統更新 + Record and Upload Microphone Audio + 錄製並上傳麥克風音訊 - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - 需要進行作業系統更新。建議將您的裝置連接上 Wi-Fi 獲得更快的更新下載。下載大小約為 1GB。 - - - Connect to Wi-Fi - 連接到無線網路 - - - Install - 安裝 - - - Back - 回上頁 - - - Loading... - 載入中… - - - Reboot - 重新啟動 - - - Update failed - 更新失敗 + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + 在駕駛時錄製並儲存麥克風音訊。此音訊將會收錄在 comma connect 的行車記錄器影片中。 @@ -2075,12 +1146,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/widgets/exp_mode_button.py b/selfdrive/ui/widgets/exp_mode_button.py index 83caf413d1..be50c59d06 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): diff --git a/selfdrive/ui/widgets/offroad_alerts.py b/selfdrive/ui/widgets/offroad_alerts.py index aba07139db..f54bd9116c 100644 --- a/selfdrive/ui/widgets/offroad_alerts.py +++ b/selfdrive/ui/widgets/offroad_alerts.py @@ -5,11 +5,11 @@ from collections.abc import Callable from dataclasses import dataclass from openpilot.common.params import Params from openpilot.system.hardware import HARDWARE -from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.wrap_text import wrap_text -from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.widgets import Widget class AlertColors: @@ -190,14 +190,10 @@ class OffroadAlert(AbstractAlert): for alert_data in self.sorted_alerts: text = "" - bytes_data = self.params.get(alert_data.key) + alert_json = self.params.get(alert_data.key) - if bytes_data: - try: - alert_json = json.loads(bytes_data) - text = alert_json.get("text", "").replace("{}", alert_json.get("extra", "")) - except json.JSONDecodeError: - text = "" + if alert_json: + text = alert_json.get("text", "").replace("{}", alert_json.get("extra", "")) alert_data.text = text alert_data.visible = bool(text) @@ -296,7 +292,7 @@ class UpdateAlert(AbstractAlert): def refresh(self) -> bool: update_available: bool = self.params.get_bool("UpdateAvailable") if update_available: - self.release_notes = self.params.get("UpdaterNewReleaseNotes", encoding='utf-8') + self.release_notes = self.params.get("UpdaterNewReleaseNotes") self._cached_content_height = 0 return update_available diff --git a/selfdrive/ui/widgets/pairing_dialog.py b/selfdrive/ui/widgets/pairing_dialog.py index 2cb1499499..63298914ea 100644 --- a/selfdrive/ui/widgets/pairing_dialog.py +++ b/selfdrive/ui/widgets/pairing_dialog.py @@ -23,7 +23,7 @@ class PairingDialog: def _get_pairing_url(self) -> str: try: - dongle_id = self.params.get("DongleId", encoding='utf8') or "" + dongle_id = self.params.get("DongleId") or "" token = Api(dongle_id).get_token() except Exception as e: cloudlog.warning(f"Failed to get pairing token: {e}") @@ -55,7 +55,7 @@ class PairingDialog: self.qr_texture = None def _check_qr_refresh(self) -> None: - current_time = time.time() + current_time = time.monotonic() if current_time - self.last_qr_generation >= self.QR_REFRESH_INTERVAL: self._generate_qr_code() self.last_qr_generation = current_time diff --git a/selfdrive/ui/widgets/prime.py b/selfdrive/ui/widgets/prime.py index afdfa33e7a..86234ee149 100644 --- a/selfdrive/ui/widgets/prime.py +++ b/selfdrive/ui/widgets/prime.py @@ -2,10 +2,10 @@ import pyray as rl from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.label import gui_label -from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.label import gui_label class PrimeWidget(Widget): diff --git a/selfdrive/ui/widgets/setup.py b/selfdrive/ui/widgets/setup.py index 4fd56aa522..35a7c4101c 100644 --- a/selfdrive/ui/widgets/setup.py +++ b/selfdrive/ui/widgets/setup.py @@ -1,11 +1,11 @@ import pyray as rl from openpilot.selfdrive.ui.lib.prime_state import PrimeType -from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.wrap_text import wrap_text -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle class SetupWidget(Widget): diff --git a/selfdrive/ui/widgets/ssh_key.py b/selfdrive/ui/widgets/ssh_key.py index 418867c86f..4f4a8dcdff 100644 --- a/selfdrive/ui/widgets/ssh_key.py +++ b/selfdrive/ui/widgets/ssh_key.py @@ -6,8 +6,12 @@ from enum import Enum from openpilot.common.params import Params from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.list_view import ( +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets import DialogResult +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle +from openpilot.system.ui.widgets.confirm_dialog import alert_dialog +from openpilot.system.ui.widgets.keyboard import Keyboard +from openpilot.system.ui.widgets.list_view import ( ItemAction, ListItem, BUTTON_HEIGHT, @@ -15,10 +19,6 @@ from openpilot.system.ui.lib.list_view import ( BUTTON_FONT_SIZE, BUTTON_WIDTH, ) -from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.widget import DialogResult -from openpilot.system.ui.widgets.confirm_dialog import alert_dialog -from openpilot.system.ui.widgets.keyboard import Keyboard class SshKeyActionState(Enum): @@ -42,7 +42,7 @@ class SshKeyAction(ItemAction): self._refresh_state() def _refresh_state(self): - self._username = self._params.get("GithubUsername", "") + self._username = self._params.get("GithubUsername") self._state = SshKeyActionState.REMOVE if self._params.get("GithubSshKeys") else SshKeyActionState.ADD def _render(self, rect: rl.Rectangle) -> bool: diff --git a/sunnypilot/livedelay/__init__.py b/sunnypilot/livedelay/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sunnypilot/livedelay/lagd_toggle.py b/sunnypilot/livedelay/lagd_toggle.py new file mode 100644 index 0000000000..ee135fb877 --- /dev/null +++ b/sunnypilot/livedelay/lagd_toggle.py @@ -0,0 +1,51 @@ +""" +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 LagdToggle: + def __init__(self): + self.params = Params() + self.lag = 0.0 + self._last_desc = None + + @property + def software_delay(self): + return self.params.get("LagdToggleDelay") + + def _maybe_update_desc(self, desc): + if desc != self._last_desc: + self.params.put_nonblocking("LagdToggleDesc", desc) + self._last_desc = desc + + def lagd_main(self, CP, sm, model): + if self.params.get_bool("LagdToggle"): + lateral_delay = sm["liveDelay"].lateralDelay + lat_smooth = model.LAT_SMOOTH_SECONDS + result = lateral_delay + lat_smooth + desc = f"live steer delay learner ({lateral_delay:.3f}s) + model smoothing filter ({lat_smooth:.3f}s) = total delay ({result:.3f}s)" + self._maybe_update_desc(desc) + return result + + steer_actuator_delay = CP.steerActuatorDelay + lat_smooth = model.LAT_SMOOTH_SECONDS + delay = self.software_delay + result = (steer_actuator_delay + delay) + lat_smooth + desc = (f"Vehicle steering delay ({steer_actuator_delay:.3f}s) + software delay ({delay:.3f}s) + " + + f"model smoothing filter ({lat_smooth:.3f}s) = total delay ({result:.3f}s)") + self._maybe_update_desc(desc) + return result + + def lagd_torqued_main(self, CP, msg): + if self.params.get_bool("LagdToggle"): + self.lag = msg.lateralDelay + cloudlog.debug(f"TORQUED USING LIVE DELAY: {self.lag:.3f}") + else: + self.lag = CP.steerActuatorDelay + self.software_delay + cloudlog.debug(f"TORQUED USING STEER ACTUATOR: {self.lag:.3f}") + return self.lag 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..84b3aa9955 100644 --- a/sunnypilot/mapd/live_map_data/osm_map_data.py +++ b/sunnypilot/mapd/live_map_data/osm_map_data.py @@ -31,13 +31,13 @@ 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_str = self.mem_params.get("NextMapSpeedLimit") next_speed_limit_section = json.loads(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') 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..4e61ffe837 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,7 +56,7 @@ 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({ @@ -100,12 +98,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 +115,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..10725efb71 100755 --- a/sunnypilot/modeld/modeld.py +++ b/sunnypilot/modeld/modeld.py @@ -25,7 +25,7 @@ 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.livedelay.lagd_toggle import LagdToggle from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext @@ -62,7 +62,7 @@ class ModelState: 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,7 +202,7 @@ def main(demo=False): cloudlog.info("modeld got CarParams: %s", CP.brand) - modeld_lagd = ModeldLagd() + modeld_lagd = LagdToggle() # Enable lagd support for sunnypilot modeld long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS 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..93a8a3f05e 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -23,7 +23,7 @@ from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFr from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants from openpilot.sunnypilot.models.helpers import get_active_bundle -from openpilot.sunnypilot.models.modeld_lagd import ModeldLagd +from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle from openpilot.sunnypilot.models.runners.helpers import get_model_runner PROCESS_NAME = "selfdrive.modeld.modeld" @@ -54,10 +54,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 +86,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 +155,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 +169,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: @@ -239,7 +243,7 @@ def main(demo=False): cloudlog.info("modeld got CarParams: %s", CP.brand) - modeld_lagd = ModeldLagd() + modeld_lagd = LagdToggle() # TODO Move smooth seconds to action function long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS diff --git a/sunnypilot/modeld_v2/parse_model_outputs_split.py b/sunnypilot/modeld_v2/parse_model_outputs_split.py index 7fab66e038..79fa8ec2f2 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,64 @@ 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.PLAN_WIDTH * SplitModelConstants.IDX_N: + self._parse_plan_mhp(outs) + elif self.generation >= 12: + 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..c96fa75c9d 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -88,8 +88,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,7 +98,7 @@ 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 @@ -110,12 +110,12 @@ class ModelCache: 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._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_v6.json" def __init__(self, params: Params): self.params = params diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index a614447ceb..40fcf13ec0 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -19,8 +19,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 = 8 +REQUIRED_MIN_SELECTOR_VERSION = 8 USE_ONNX = os.getenv('USE_ONNX', PC) @@ -63,6 +64,7 @@ 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: @@ -110,7 +112,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..bdcec64c71 100644 --- a/sunnypilot/models/manager.py +++ b/sunnypilot/models/manager.py @@ -169,14 +169,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 +189,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..d104e049b7 100644 --- a/sunnypilot/models/tests/model_hash +++ b/sunnypilot/models/tests/model_hash @@ -1 +1 @@ -71979b29c4bab3007de1a4265442d79f44c0eaef066af66086dddfc432709b94 \ No newline at end of file +71c14a02af81857565e5fcffaaddb57a76fe80b283fe3292da830dbc92e211b7 \ No newline at end of file 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 e2a232a3ef..28bae4a0dd 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -30,7 +30,7 @@ def _initialize_custom_longitudinal_tuning(CI: CarInterfaceBase, CP: structs.Car # Hyundai Custom Longitudinal Tuning if CP.brand == 'hyundai': - hyundai_longitudinal_tuning = int(params.get("HyundaiLongitudinalTuning", encoding="utf8") or 0) + hyundai_longitudinal_tuning = params.get("HyundaiLongitudinalTuning") if hyundai_longitudinal_tuning == LongitudinalTuningType.DYNAMIC: CP_SP.flags |= HyundaiFlagsSP.LONG_TUNING_DYNAMIC.value if hyundai_longitudinal_tuning == LongitudinalTuningType.PREDICTIVE: diff --git a/sunnypilot/selfdrive/car/tests/test_custom_cruise.py b/sunnypilot/selfdrive/car/tests/test_custom_cruise.py index 2f9d976847..4b7f9b819b 100644 --- a/sunnypilot/selfdrive/car/tests/test_custom_cruise.py +++ b/sunnypilot/selfdrive/car/tests/test_custom_cruise.py @@ -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..6f8d99b755 100644 --- a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py +++ b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py @@ -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..43d652e8b7 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py @@ -10,6 +10,8 @@ import numpy as np from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from openpilot.selfdrive.modeld.constants import ModelConstants +from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle + LAT_PLAN_MIN_IDX = 5 LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan @@ -41,11 +43,13 @@ def get_lookahead_value(future_vals, current_val): return min_val -class LatControlTorqueExtBase: +class LatControlTorqueExtBase(LagdToggle): def __init__(self, lac_torque, CP, CP_SP): + LagdToggle.__init__(self) 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.use_steering_angle = True # FIXME-SP: deprecated in upstream self.actual_lateral_jerk: float = 0.0 self.lateral_jerk_setpoint: float = 0.0 @@ -53,7 +57,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 diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 5af1c611de..e3dee73912 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -11,6 +11,7 @@ from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP +from openpilot.sunnypilot.models.helpers import get_active_bundle DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState @@ -20,8 +21,14 @@ class LongitudinalPlannerSP: self.events_sp = EventsSP() self.dec = DynamicExperimentalController(CP, mpc) + self.generation = int(model_bundle.generation) if (model_bundle := get_active_bundle()) else None self.slc = SpeedLimitController(CP) + @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(): return None diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py index 2f89594c8f..4872f5c88c 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -126,8 +126,7 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): self._ff = self.model.evaluate(nn_input) # apply friction override for cars with low NN friction response + # TODO-SP: verify with twilsonco if this change is appropriate 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) 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/sunnylink/api.py b/sunnypilot/sunnylink/api.py index eb9343af9a..b7838a40b0 100644 --- a/sunnypilot/sunnylink/api.py +++ b/sunnypilot/sunnylink/api.py @@ -46,8 +46,8 @@ class SunnylinkApi(BaseApi): time.sleep(0.5) def _resolve_dongle_ids(self): - sunnylink_dongle_id = self.params.get("SunnylinkDongleId", encoding='utf-8') - comma_dongle_id = self.dongle_id or self.params.get("DongleId", encoding='utf-8') + sunnylink_dongle_id = self.params.get("SunnylinkDongleId") + comma_dongle_id = self.dongle_id or self.params.get("DongleId") return sunnylink_dongle_id, comma_dongle_id def _resolve_imeis(self): @@ -63,7 +63,7 @@ class SunnylinkApi(BaseApi): return imei1, imei2 def _resolve_serial(self): - return (self.params.get("HardwareSerial", encoding='utf8') + return (self.params.get("HardwareSerial") or HARDWARE.get_serial()) def register_device(self, spinner=None, timeout=60, verbose=False): @@ -90,7 +90,7 @@ class SunnylinkApi(BaseApi): sunnylink_dongle_id = UNREGISTERED_SUNNYLINK_DONGLE_ID self._status_update("Public key not found, setting dongle ID to unregistered.") else: - Params().put("LastSunnylinkPingTime", "0") # Reset the last ping time to 0 if we are trying to register + Params().put("LastSunnylinkPingTime", 0) # Reset the last ping time to 0 if we are trying to register with pubkey_path.open() as f1, privkey_path.open() as f2: public_key = f1.read() private_key = f2.read() @@ -148,7 +148,7 @@ class SunnylinkApi(BaseApi): # Set the last ping time to the current time since we were just talking to the API last_ping = int(time.monotonic() * 1e9) if successful_registration else start_time - Params().put("LastSunnylinkPingTime", str(last_ping)) + Params().put("LastSunnylinkPingTime", last_ping) # Disable sunnylink if registration was not successful if not successful_registration: diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/sunnypilot/sunnylink/athena/sunnylinkd.py index a675820628..42b7d779e3 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -31,7 +31,7 @@ SUNNYLINK_RECONNECT_TIMEOUT_S = 70 # FYI changing this will also would require DISALLOW_LOG_UPLOAD = threading.Event() params = Params() -sunnylink_dongle_id = params.get("SunnylinkDongleId", encoding='utf-8') +sunnylink_dongle_id = params.get("SunnylinkDongleId") sunnylink_api = SunnylinkApi(sunnylink_dongle_id) @@ -68,7 +68,7 @@ def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None: end_event.set() comma_prime_cellular_end_event.set() - prime_type = params.get("PrimeType", encoding='utf-8') or 0 + prime_type = params.get("PrimeType") or 0 metered = sm['deviceState'].networkMetered if DISALLOW_LOG_UPLOAD.is_set() and not comma_prime_cellular_end_event.is_set(): @@ -103,7 +103,7 @@ def ws_recv(ws: WebSocket, end_event: threading.Event) -> None: elif opcode in (ABNF.OPCODE_PING, ABNF.OPCODE_PONG): cloudlog.debug("sunnylinkd.ws_recv.pong") last_ping = int(time.monotonic() * 1e9) - Params().put("LastSunnylinkPingTime", str(last_ping)) + Params().put("LastSunnylinkPingTime", last_ping) except WebSocketTimeoutException: ns_since_last_ping = int(time.monotonic() * 1e9) - last_ping if ns_since_last_ping > SUNNYLINK_RECONNECT_TIMEOUT_S * 1e9: diff --git a/sunnypilot/sunnylink/backups/manager.py b/sunnypilot/sunnylink/backups/manager.py index d6408d7ba7..5110782a1c 100644 --- a/sunnypilot/sunnylink/backups/manager.py +++ b/sunnypilot/sunnylink/backups/manager.py @@ -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"]) @@ -253,7 +253,7 @@ class BackupManagerSP: self.params.remove("BackupManager_CreateBackup") # Check for restore command - restore_version = self.params.get("BackupManager_RestoreVersion", encoding="utf8") + restore_version = self.params.get("BackupManager_RestoreVersion") if restore_version: try: version = int(restore_version) if restore_version.isdigit() else None diff --git a/sunnypilot/sunnylink/uploader.py b/sunnypilot/sunnylink/uploader.py index 69ef2cfd8a..b9117f2866 100755 --- a/sunnypilot/sunnylink/uploader.py +++ b/sunnypilot/sunnylink/uploader.py @@ -92,7 +92,7 @@ class Uploader: self.immediate_priority = {"qlog": 0, "qlog.bz2": 0, "qcamera.ts": 1} def list_upload_files(self, metered: bool) -> Iterator[tuple[str, str, str]]: - r = self.params.get("AthenadRecentlyViewedRoutes", encoding="utf8") + r = self.params.get("AthenadRecentlyViewedRoutes") requested_routes = [] if r is None else r.split(",") for logdir in listdir_by_creation(self.root): @@ -250,7 +250,7 @@ def main(exit_event: threading.Event = None) -> None: clear_locks(Paths.log_root()) params = Params() - dongle_id = params.get("SunnylinkDongleId", encoding='utf8') + dongle_id = params.get("SunnylinkDongleId") offroad_transition_prev = 0. offroad_last = False diff --git a/sunnypilot/sunnylink/utils.py b/sunnypilot/sunnylink/utils.py index 55b1f6f285..35714eafe3 100644 --- a/sunnypilot/sunnylink/utils.py +++ b/sunnypilot/sunnylink/utils.py @@ -7,7 +7,7 @@ def get_sunnylink_status(params=None) -> tuple[bool, bool]: """Get the status of Sunnylink on the device. Returns a tuple of (is_sunnylink_enabled, is_registered).""" params = params or Params() is_sunnylink_enabled = params.get_bool("SunnylinkEnabled") - is_registered = params.get("SunnylinkDongleId", encoding='utf-8') not in (None, UNREGISTERED_SUNNYLINK_DONGLE_ID) + is_registered = params.get("SunnylinkDongleId") not in (None, UNREGISTERED_SUNNYLINK_DONGLE_ID) return is_sunnylink_enabled, is_registered @@ -51,7 +51,7 @@ def register_sunnylink(): def get_api_token(): """Get the API token for the device.""" params = Params() - sunnylink_dongle_id = params.get("SunnylinkDongleId", encoding='utf-8') + sunnylink_dongle_id = params.get("SunnylinkDongleId") sunnylink_api = SunnylinkApi(sunnylink_dongle_id) token = sunnylink_api.get_token() print(f"API Token: {token}") diff --git a/system/athena/athenad.py b/system/athena/athenad.py index 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.py b/system/hardware/base.py index 3506be5145..b457ea4e17 100644 --- a/system/hardware/base.py +++ b/system/hardware/base.py @@ -199,9 +199,6 @@ class HardwareBase(ABC): def get_modem_temperatures(self): pass - @abstractmethod - def get_nvme_temperatures(self): - pass @abstractmethod def initialize_hardware(self): diff --git a/system/hardware/hardwared.py b/system/hardware/hardwared.py index 223faeaafd..7f9d971fbf 100755 --- a/system/hardware/hardwared.py +++ b/system/hardware/hardwared.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import fcntl import os -import json import queue import struct import threading @@ -19,13 +18,14 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_HW from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert -from openpilot.system.hardware import HARDWARE, TICI, AGNOS +from openpilot.system.hardware import HARDWARE, TICI, AGNOS, PC from openpilot.system.loggerd.config import get_available_percent from openpilot.system.statsd import statlog from openpilot.common.swaglog import cloudlog from openpilot.system.hardware.power_monitoring import PowerMonitoring from openpilot.system.hardware.fan_controller import TiciFanController from openpilot.system.version import terms_version, training_version +from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID ThermalStatus = log.DeviceState.ThermalStatus NetworkType = log.DeviceState.NetworkType @@ -38,7 +38,7 @@ ONROAD_CYCLE_TIME = 1 # seconds to wait offroad after requesting an onroad cycl ThermalBand = namedtuple("ThermalBand", ['min_temp', 'max_temp']) HardwareState = namedtuple("HardwareState", ['network_type', 'network_info', 'network_strength', 'network_stats', - 'network_metered', 'nvme_temps', 'modem_temps']) + 'network_metered', 'modem_temps']) # List of thermal bands. We will stay within this region as long as we are within the bounds. # When exiting the bounds, we'll jump to the lower or higher band. Bands are ordered in the dict. @@ -141,7 +141,6 @@ def hw_state_thread(end_event, hw_queue): network_strength=HARDWARE.get_network_strength(network_type), network_stats={'wwanTx': tx, 'wwanRx': rx}, network_metered=HARDWARE.get_network_metered(network_type), - nvme_temps=HARDWARE.get_nvme_temperatures(), modem_temps=modem_temps, ) @@ -172,6 +171,7 @@ def hardware_thread(end_event, hw_queue) -> None: onroad_conditions: dict[str, bool] = { "ignition": False, "not_onroad_cycle": True, + "device_temp_good": True, } startup_conditions: dict[str, bool] = {} startup_conditions_prev: dict[str, bool] = {} @@ -188,7 +188,6 @@ def hardware_thread(end_event, hw_queue) -> None: network_metered=False, network_strength=NetworkStrength.unknown, network_stats={'wwanTx': -1, 'wwanRx': -1}, - nvme_temps=[], modem_temps=[], ) @@ -202,6 +201,10 @@ def hardware_thread(end_event, hw_queue) -> None: params = Params() power_monitor = PowerMonitoring() + uptime_offroad: float = params.get("UptimeOffroad", return_default=True) + uptime_onroad: float = params.get("UptimeOnroad", return_default=True) + last_uptime_ts: float = time.monotonic() + HARDWARE.initialize_hardware() thermal_config = HARDWARE.get_thermal_config() @@ -267,7 +270,6 @@ def hardware_thread(end_event, hw_queue) -> None: if last_hw_state.network_info is not None: msg.deviceState.networkInfo = last_hw_state.network_info - msg.deviceState.nvmeTempC = last_hw_state.nvme_temps msg.deviceState.modemTempC = last_hw_state.modem_temps msg.deviceState.screenBrightnessPercent = HARDWARE.get_screen_brightness() @@ -304,6 +306,7 @@ def hardware_thread(end_event, hw_queue) -> None: # **** starting logic **** startup_conditions["up_to_date"] = params.get("Offroad_ConnectivityNeeded") is None or params.get_bool("DisableUpdates") or params.get_bool("SnoozeUpdate") + startup_conditions["no_excessive_actuation"] = params.get("Offroad_ExcessiveActuation") is None startup_conditions["not_uninstalling"] = not params.get_bool("DoUninstall") startup_conditions["accepted_terms"] = params.get("HasAcceptedTerms") == terms_version @@ -330,21 +333,17 @@ def hardware_thread(end_event, hw_queue) -> None: show_alert = (not onroad_conditions["device_temp_good"] or not startup_conditions["device_temp_engageable"]) and onroad_conditions["ignition"] set_offroad_alert_if_changed("Offroad_TemperatureTooHigh", show_alert, extra_text=extra_text) + # *** registration check *** + if not PC: + # we enforce this for our software, but you are welcome + # to make a different decision in your software + startup_conditions["registered_device"] = PC or (params.get("DongleId") != UNREGISTERED_DONGLE_ID) + # TODO: this should move to TICI.initialize_hardware, but we currently can't import params there if TICI and HARDWARE.get_device_type() == "tici": if not os.path.isfile("/persist/comma/living-in-the-moment"): if not Path("/data/media").is_mount(): set_offroad_alert_if_changed("Offroad_StorageMissing", True) - else: - # check for bad NVMe - try: - with open("/sys/block/nvme0n1/device/model") as f: - model = f.read().strip() - if not model.startswith("Samsung SSD 980") and params.get("Offroad_BadNvme") is None: - set_offroad_alert_if_changed("Offroad_BadNvme", True) - cloudlog.event("Unsupported NVMe", model=model, error=True) - except Exception: - pass # Handle offroad/onroad transition should_start = all(onroad_conditions.values()) @@ -411,7 +410,7 @@ def hardware_thread(end_event, hw_queue) -> None: last_ping = params.get("LastAthenaPingTime") if last_ping is not None: - msg.deviceState.lastAthenaPingTime = int(last_ping) + msg.deviceState.lastAthenaPingTime = last_ping msg.deviceState.thermalStatus = thermal_status pm.send("deviceState", msg) @@ -429,8 +428,6 @@ def hardware_thread(end_event, hw_queue) -> None: statlog.gauge("memory_temperature", msg.deviceState.memoryTempC) for i, temp in enumerate(msg.deviceState.pmicTempC): statlog.gauge(f"pmic{i}_temperature", temp) - for i, temp in enumerate(last_hw_state.nvme_temps): - statlog.gauge(f"nvme_temperature{i}", temp) for i, temp in enumerate(last_hw_state.modem_temps): statlog.gauge(f"modem_temperature{i}", temp) statlog.gauge("fan_speed_percent_desired", msg.deviceState.fanSpeedPercentDesired) @@ -451,12 +448,23 @@ def hardware_thread(end_event, hw_queue) -> None: # save last one before going onroad if rising_edge_started: try: - params.put("LastOffroadStatusPacket", json.dumps(dat)) + params.put("LastOffroadStatusPacket", dat) except Exception: cloudlog.exception("failed to save offroad status") params.put_bool_nonblocking("NetworkMetered", msg.deviceState.networkMetered) + now_ts = time.monotonic() + if off_ts: + uptime_offroad += now_ts - max(last_uptime_ts, off_ts) + elif started_ts: + uptime_onroad += now_ts - max(last_uptime_ts, started_ts) + last_uptime_ts = now_ts + + if (count % int(60. / DT_HW)) == 0: + params.put("UptimeOffroad", uptime_offroad) + params.put("UptimeOnroad", uptime_onroad) + count += 1 should_start_prev = should_start diff --git a/system/hardware/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/hardware.h b/system/hardware/tici/hardware.h index 179ef54a9b..ae1087fa73 100644 --- a/system/hardware/tici/hardware.h +++ b/system/hardware/tici/hardware.h @@ -75,7 +75,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/ffmpeg_encoder.cc b/system/loggerd/encoder/ffmpeg_encoder.cc index 4e6946363f..091c2fb2dd 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 @@ -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/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..144ab9f349 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) { @@ -214,7 +222,7 @@ void loggerd_thread() { typedef struct ServiceState { std::string name; int counter, freq; - bool encoder, user_flag; + bool encoder, user_flag, 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, + .user_flag = it.name == "userFlag", + .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) { @@ -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..5dfb178fd5 100644 --- a/system/loggerd/loggerd.h +++ b/system/loggerd/loggerd.h @@ -35,6 +35,7 @@ public: 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; @@ -106,6 +107,7 @@ const EncoderInfo qcam_encoder_info = { .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .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_loggerd.py b/system/loggerd/tests/test_loggerd.py index 179e237a65..8f9d92e104 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 @@ -281,3 +295,30 @@ class TestLoggerd: 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 e78ae477af..a567c30f8f 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -8,7 +8,7 @@ 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 +19,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,68 +26,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"), - ("Mads", "1"), - ("MadsMainCruiseAllowed", "1"), - ("MadsSteeringMode", "0"), - ("MadsUnifiedEngagementMode", "1"), - ("MapdVersion", f"{VERSION}"), - ("MaxTimeOffroad", "1800"), - ("Brightness", "0"), - ("ModelManager_LastSyncTime", "0"), - ("ModelManager_ModelsCache", ""), - ("NeuralNetworkLateralControl", "0"), - ("QuietMode", "0"), - - ("SpeedLimitControl", "0"), - ("SpeedLimitControlPolicy", "3"), - ("SpeedLimitEngageType", "0"), - ("SpeedLimitOffsetType", "0"), - ("SpeedLimitValueOffset", "0"), - ("SpeedLimitWarningType", "0"), - ("SpeedLimitWarningOffsetType", "0"), - ("SpeedLimitWarningValueOffset", "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: @@ -108,6 +63,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) @@ -161,19 +117,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) @@ -181,15 +138,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) diff --git a/system/manager/process.py b/system/manager/process.py index 0ebddae049..c83cc46e0d 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) diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 2e3f754a76..6cdb89d980 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)) @@ -162,7 +165,7 @@ 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), ] 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/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..c7fe39adf9 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -3,21 +3,27 @@ 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 -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 +33,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 +51,68 @@ class ModalOverlay: callback: Callable | None = None +class MousePos(NamedTuple): + x: float + y: float + + +class MouseEvent(NamedTuple): + pos: MousePos + 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: MouseEvent | None = None + + 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): + mouse_pos = rl.get_mouse_position() + ev = MouseEvent( + MousePos(mouse_pos.x, mouse_pos.y), + rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT), + rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT), + rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT), + time.monotonic(), + ) + # Only add changes + if self._prev_mouse_event is None or ev[:-1] != self._prev_mouse_event[:-1]: + with self._lock: + self._events.append(ev) + self._prev_mouse_event = ev + + class GuiApplication: def __init__(self, width: int, height: int): self._fonts: dict[FontWeight, rl.Font] = {} @@ -61,6 +129,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 +163,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 +226,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 +281,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 +312,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/scroll_panel.py b/system/ui/lib/scroll_panel.py index 32f3b7b575..21b6795a75 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,54 @@ 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), False, False, False, time.monotonic())]: + 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 +101,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 +172,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..e4ee224d53 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 @@ -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')}, } diff --git a/system/ui/reset.py b/system/ui/reset.py index c0dbd1d956..7c9500c7cf 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,56 @@ 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 or self._reset_state == ResetState.FAILED: + self._reboot_button.render(rl.Rectangle(rect.x, button_top, rect.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)) - 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." diff --git a/system/ui/setup.py b/system/ui/setup.py index 4d9d269480..060d8cf4e3 100755 --- a/system/ui/setup.py +++ b/system/ui/setup.py @@ -10,11 +10,11 @@ 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 gui_button, ButtonStyle from openpilot.system.ui.widgets.keyboard import Keyboard +from openpilot.system.ui.widgets.label import gui_label, gui_text_box +from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManagerWrapper NetworkType = log.DeviceState.NetworkType 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 80% rename from system/ui/lib/widget.py rename to system/ui/widgets/__init__.py index 3539f5594b..7a436b8e9f 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 class DialogResult(IntEnum): @@ -66,18 +67,18 @@ 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 + for mouse_event in gui_app.mouse_events: + if mouse_event.left_pressed and self._touch_valid(): + if rl.check_collision_point_rec(mouse_event.pos, self._rect): + self._is_pressed = True - elif not self._touch_valid(): - self._is_pressed = False + elif not self._touch_valid(): + self._is_pressed = False - 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 + elif mouse_event.left_released: + if self._is_pressed and rl.check_collision_point_rec(mouse_event.pos, self._rect): + self._handle_mouse_release(mouse_event.pos) + self._is_pressed = False return ret @@ -91,6 +92,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 75% rename from system/ui/lib/button.py rename to system/ui/widgets/button.py index 121e7f86fa..2be56e6dd5 100644 --- a/system/ui/lib/button.py +++ b/system/ui/widgets/button.py @@ -1,7 +1,11 @@ -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 class ButtonStyle(IntEnum): @@ -54,6 +58,8 @@ BUTTON_PRESSED_BACKGROUND_COLORS = { _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 +152,44 @@ 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, + ): + + super().__init__() + self._text = text + self._click_callback = click_callback + self._label_font = gui_app.font(FontWeight.SEMI_BOLD) + self._button_style = button_style + self._font_size = font_size + self._border_radius = border_radius + self._font_size = font_size + self._text_color = BUTTON_TEXT_COLOR[button_style] + self._text_size = measure_text_cached(gui_app.font(font_weight), text, font_size) + + def _handle_mouse_release(self, mouse_pos: MousePos): + if self._click_callback: + print(f"Button clicked: {self._text}") + self._click_callback() + + def _get_background_color(self) -> rl.Color: + if self._is_pressed: + return BUTTON_PRESSED_BACKGROUND_COLORS[self._button_style] + else: + return BUTTON_BACKGROUND_COLORS[self._button_style] + + def _render(self, _): + roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2) + rl.draw_rectangle_rounded(self._rect, roundness, 10, self._get_background_color()) + + text_pos = rl.Vector2(0, self._rect.y + (self._rect.height - self._text_size.y) // 2) + text_pos.x = self._rect.x + (self._rect.width - self._text_size.x) // 2 + rl.draw_text_ex(self._label_font, self._text, text_pos, self._font_size, 0, self._text_color) diff --git a/system/ui/widgets/confirm_dialog.py b/system/ui/widgets/confirm_dialog.py index 00361905d6..647a455d68 100644 --- a/system/ui/widgets/confirm_dialog.py +++ b/system/ui/widgets/confirm_dialog.py @@ -1,8 +1,8 @@ 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 +from openpilot.system.ui.widgets.label import gui_text_box DIALOG_WIDTH = 1520 DIALOG_HEIGHT = 600 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 99% rename from system/ui/lib/inputbox.py rename to system/ui/widgets/inputbox.py index ccf718ba56..65428479bf 100644 --- a/system/ui/lib/inputbox.py +++ b/system/ui/widgets/inputbox.py @@ -2,7 +2,7 @@ import pyray as rl import time 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.widgets import Widget PASSWORD_MASK_CHAR = "•" PASSWORD_MASK_DELAY = 1.5 # Seconds to show character before masking diff --git a/system/ui/widgets/keyboard.py b/system/ui/widgets/keyboard.py index 338706e73d..432d9b3cf3 100644 --- a/system/ui/widgets/keyboard.py +++ b/system/ui/widgets/keyboard.py @@ -2,10 +2,10 @@ 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, gui_button +from openpilot.system.ui.widgets.inputbox import InputBox +from openpilot.system.ui.widgets.label import gui_label KEY_FONT_SIZE = 96 DOUBLE_CLICK_THRESHOLD = 0.5 # seconds diff --git a/system/ui/lib/label.py b/system/ui/widgets/label.py similarity index 98% rename from system/ui/lib/label.py rename to system/ui/widgets/label.py index c3d0e0303a..e840e8586a 100644 --- a/system/ui/lib/label.py +++ b/system/ui/widgets/label.py @@ -4,6 +4,8 @@ from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.utils import GuiStyleContext +# TODO: This should be a Widget class + def gui_label( rect: rl.Rectangle, text: str, diff --git a/system/ui/lib/list_view.py b/system/ui/widgets/list_view.py similarity index 98% rename from system/ui/lib/list_view.py rename to system/ui/widgets/list_view.py index f5581be23e..aa2fdb845d 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 @@ -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..63e26f0cc6 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -4,13 +4,13 @@ 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.keyboard import Keyboard +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import ButtonStyle, gui_button from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets.keyboard import Keyboard +from openpilot.system.ui.widgets.label import gui_label NM_DEVICE_STATE_NEED_AUTH = 60 MIN_PASSWORD_LENGTH = 8 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 93% rename from system/ui/lib/toggle.py rename to system/ui/widgets/toggle.py index 8ed9a655ec..3f7f463916 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 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/tinygrad_repo b/tinygrad_repo index 519dec6677..7737cbb2a0 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 519dec6677f98718ee4f2d07be1936eb91dde73b +Subproject commit 7737cbb2a0635fce95a9085fb1b53d5bea1093f8 diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index c8e6093b86..91edfafe00 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -29,7 +29,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', + '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'): 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/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/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..b817ae1208 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -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/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..1075bd2f08 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,16 @@ 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 QCAMERA_FILENAMES, CAMERA_FILENAMES, DCAMERA_FILENAMES, \ + ECAMERA_FILENAMES, BOOTLOG_FILENAMES, Route, SegmentRange from openpilot.tools.lib.log_time_series import msgs_to_time_series LogMessage = type[capnp._DynamicStructReader] @@ -96,14 +98,23 @@ 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 +class FileName(enum.Enum): + #TODO use the ones from route.py + RLOG = ("rlog.zst", "rlog.bz2") + QLOG = ("qlog.zst", "qlog.bz2") + QCAMERA = QCAMERA_FILENAMES + FCAMERA = CAMERA_FILENAMES + ECAMERA = ECAMERA_FILENAMES + DCAMERA = DCAMERA_FILENAMES + BOOTLOG = BOOTLOG_FILENAMES + + LogPath = str | None -ValidFileCallable = Callable[[LogPath], bool] -Source = Callable[[SegmentRange, ReadMode], list[LogPath]] +Source = Callable[[SegmentRange, FileName], list[LogPath]] InternalUnavailableException = Exception("Internal source not available") @@ -112,139 +123,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: FileName) -> 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: FileName, 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.value] 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: FileName) -> list[LogPath]: + return eval_source([[get_url(sr.route_name, seg, fn) for fn in fns.value] 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: FileName) -> 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 +256,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 +265,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..55ea956c5f 100644 --- a/tools/lib/route.py +++ b/tools/lib/route.py @@ -10,12 +10,13 @@ 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'] +QLOG_FILENAMES = ('qlog.bz2', 'qlog.zst', 'qlog') +QCAMERA_FILENAMES = ('qcamera.ts',) +LOG_FILENAMES = ('rlog.bz2', 'raw_log.bz2', 'rlog.zst', 'rlog') +CAMERA_FILENAMES = ('fcamera.hevc', 'video.hevc') +DCAMERA_FILENAMES = ('dcamera.hevc',) +ECAMERA_FILENAMES = ('ecamera.hevc',) +BOOTLOG_FILENAMES = ('bootlog.zst', 'bootlog.bz2', 'bootlog') class Route: @@ -215,12 +216,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 +250,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 +278,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 +363,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/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/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/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/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/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_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/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..3b8de83bd1 100644 --- a/uv.lock +++ b/uv.lock @@ -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.23.1" 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/29/1201ffbb6a57a16524dd91f3e741b4c828a70aaba436578bdcb3fbcb438c/azure_identity-1.23.1.tar.gz", hash = "sha256:226c1ef982a9f8d5dcf6e0f9ed35eaef2a4d971e7dd86317e9b9d52e70a035e4", size = 266185, upload-time = "2025-07-15T19:16:38.077Z" } 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/99/b3/e2d7ab810eb68575a5c7569b03c0228b8f4ce927ffa6211471b526f270c9/azure_identity-1.23.1-py3-none-any.whl", hash = "sha256:7eed28baa0097a47e3fb53bd35a63b769e6b085bb3cb616dfce2b67f28a004a1", size = 186810, upload-time = "2025-07-15T19:16:40.184Z" }, ] [[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.7.14" 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/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } 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/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, ] [[package]] @@ -336,73 +337,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 +479,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 +514,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 +623,7 @@ wheels = [ [[package]] name = "gymnasium" -version = "1.1.1" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloudpickle" }, @@ -663,9 +631,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]] @@ -796,64 +764,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]] @@ -1047,16 +993,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 +1019,73 @@ wheels = [ [[package]] name = "multidict" -version = "6.5.0" +version = "6.6.3" 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/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006, upload-time = "2025-06-30T15:53:46.929Z" } 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/08/f0/1a39863ced51f639c81a5463fbfa9eb4df59c20d1a8769ab9ef4ca57ae04/multidict-6.6.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:18f4eba0cbac3546b8ae31e0bbc55b02c801ae3cbaf80c247fcdd89b456ff58c", size = 76445, upload-time = "2025-06-30T15:51:24.01Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0e/a7cfa451c7b0365cd844e90b41e21fab32edaa1e42fc0c9f68461ce44ed7/multidict-6.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef43b5dd842382329e4797c46f10748d8c2b6e0614f46b4afe4aee9ac33159df", size = 44610, upload-time = "2025-06-30T15:51:25.158Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/a14a4efc5ee748cc1904b0748be278c31b9295ce5f4d2ef66526f410b94d/multidict-6.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bd1fd5eec01494e0f2e8e446a74a85d5e49afb63d75a9934e4a5423dba21d", size = 44267, upload-time = "2025-06-30T15:51:26.326Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f8/410677d563c2d55e063ef74fe578f9d53fe6b0a51649597a5861f83ffa15/multidict-6.6.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5bd8d6f793a787153956cd35e24f60485bf0651c238e207b9a54f7458b16d539", size = 230004, upload-time = "2025-06-30T15:51:27.491Z" }, + { url = "https://files.pythonhosted.org/packages/fd/df/2b787f80059314a98e1ec6a4cc7576244986df3e56b3c755e6fc7c99e038/multidict-6.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf99b4daf908c73856bd87ee0a2499c3c9a3d19bb04b9c6025e66af3fd07462", size = 247196, upload-time = "2025-06-30T15:51:28.762Z" }, + { url = "https://files.pythonhosted.org/packages/05/f2/f9117089151b9a8ab39f9019620d10d9718eec2ac89e7ca9d30f3ec78e96/multidict-6.6.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b9e59946b49dafaf990fd9c17ceafa62976e8471a14952163d10a7a630413a9", size = 225337, upload-time = "2025-06-30T15:51:30.025Z" }, + { url = "https://files.pythonhosted.org/packages/93/2d/7115300ec5b699faa152c56799b089a53ed69e399c3c2d528251f0aeda1a/multidict-6.6.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e2db616467070d0533832d204c54eea6836a5e628f2cb1e6dfd8cd6ba7277cb7", size = 257079, upload-time = "2025-06-30T15:51:31.716Z" }, + { url = "https://files.pythonhosted.org/packages/15/ea/ff4bab367623e39c20d3b07637225c7688d79e4f3cc1f3b9f89867677f9a/multidict-6.6.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7394888236621f61dcdd25189b2768ae5cc280f041029a5bcf1122ac63df79f9", size = 255461, upload-time = "2025-06-30T15:51:33.029Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/2c9246cda322dfe08be85f1b8739646f2c4c5113a1422d7a407763422ec4/multidict-6.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f114d8478733ca7388e7c7e0ab34b72547476b97009d643644ac33d4d3fe1821", size = 246611, upload-time = "2025-06-30T15:51:34.47Z" }, + { url = "https://files.pythonhosted.org/packages/a8/62/279c13d584207d5697a752a66ffc9bb19355a95f7659140cb1b3cf82180e/multidict-6.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cdf22e4db76d323bcdc733514bf732e9fb349707c98d341d40ebcc6e9318ef3d", size = 243102, upload-time = "2025-06-30T15:51:36.525Z" }, + { url = "https://files.pythonhosted.org/packages/69/cc/e06636f48c6d51e724a8bc8d9e1db5f136fe1df066d7cafe37ef4000f86a/multidict-6.6.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e995a34c3d44ab511bfc11aa26869b9d66c2d8c799fa0e74b28a473a692532d6", size = 238693, upload-time = "2025-06-30T15:51:38.278Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/66c9d8fb9acf3b226cdd468ed009537ac65b520aebdc1703dd6908b19d33/multidict-6.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766a4a5996f54361d8d5a9050140aa5362fe48ce51c755a50c0bc3706460c430", size = 246582, upload-time = "2025-06-30T15:51:39.709Z" }, + { url = "https://files.pythonhosted.org/packages/cf/01/c69e0317be556e46257826d5449feb4e6aa0d18573e567a48a2c14156f1f/multidict-6.6.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3893a0d7d28a7fe6ca7a1f760593bc13038d1d35daf52199d431b61d2660602b", size = 253355, upload-time = "2025-06-30T15:51:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/c0/da/9cc1da0299762d20e626fe0042e71b5694f9f72d7d3f9678397cbaa71b2b/multidict-6.6.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:934796c81ea996e61914ba58064920d6cad5d99140ac3167901eb932150e2e56", size = 247774, upload-time = "2025-06-30T15:51:42.291Z" }, + { url = "https://files.pythonhosted.org/packages/e6/91/b22756afec99cc31105ddd4a52f95ab32b1a4a58f4d417979c570c4a922e/multidict-6.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ed948328aec2072bc00f05d961ceadfd3e9bfc2966c1319aeaf7b7c21219183", size = 242275, upload-time = "2025-06-30T15:51:43.642Z" }, + { url = "https://files.pythonhosted.org/packages/be/f1/adcc185b878036a20399d5be5228f3cbe7f823d78985d101d425af35c800/multidict-6.6.3-cp311-cp311-win32.whl", hash = "sha256:9f5b28c074c76afc3e4c610c488e3493976fe0e596dd3db6c8ddfbb0134dcac5", size = 41290, upload-time = "2025-06-30T15:51:45.264Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d4/27652c1c6526ea6b4f5ddd397e93f4232ff5de42bea71d339bc6a6cc497f/multidict-6.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc7f6fbc61b1c16050a389c630da0b32fc6d4a3d191394ab78972bf5edc568c2", size = 45942, upload-time = "2025-06-30T15:51:46.377Z" }, + { url = "https://files.pythonhosted.org/packages/16/18/23f4932019804e56d3c2413e237f866444b774b0263bcb81df2fdecaf593/multidict-6.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:d4e47d8faffaae822fb5cba20937c048d4f734f43572e7079298a6c39fb172cb", size = 42880, upload-time = "2025-06-30T15:51:47.561Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6", size = 76514, upload-time = "2025-06-30T15:51:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f", size = 45394, upload-time = "2025-06-30T15:51:49.986Z" }, + { url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55", size = 43590, upload-time = "2025-06-30T15:51:51.331Z" }, + { url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b", size = 237292, upload-time = "2025-06-30T15:51:52.584Z" }, + { url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888", size = 258385, upload-time = "2025-06-30T15:51:53.913Z" }, + { url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d", size = 242328, upload-time = "2025-06-30T15:51:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680", size = 268057, upload-time = "2025-06-30T15:51:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a", size = 269341, upload-time = "2025-06-30T15:51:59.111Z" }, + { url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961", size = 256081, upload-time = "2025-06-30T15:52:00.533Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65", size = 253581, upload-time = "2025-06-30T15:52:02.43Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643", size = 250750, upload-time = "2025-06-30T15:52:04.26Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063", size = 251548, upload-time = "2025-06-30T15:52:06.002Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3", size = 262718, upload-time = "2025-06-30T15:52:07.707Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75", size = 259603, upload-time = "2025-06-30T15:52:09.58Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10", size = 251351, upload-time = "2025-06-30T15:52:10.947Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5", size = 41860, upload-time = "2025-06-30T15:52:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17", size = 45982, upload-time = "2025-06-30T15:52:13.6Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b", size = 43210, upload-time = "2025-06-30T15:52:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" }, ] [[package]] name = "mypy" -version = "1.16.1" +version = "1.17.0" 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/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } 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/d4/24/82efb502b0b0f661c49aa21cfe3e1999ddf64bf5500fc03b5a1536a39d39/mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be", size = 10914150, upload-time = "2025-07-14T20:31:51.985Z" }, + { url = "https://files.pythonhosted.org/packages/03/96/8ef9a6ff8cedadff4400e2254689ca1dc4b420b92c55255b44573de10c54/mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61", size = 10039845, upload-time = "2025-07-14T20:32:30.527Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/7ce359a56be779d38021d07941cfbb099b41411d72d827230a36203dbb81/mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f", size = 11837246, upload-time = "2025-07-14T20:32:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/b775047054de4d8dbd668df9137707e54b07fe18c7923839cd1e524bf756/mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d", size = 12571106, upload-time = "2025-07-14T20:34:26.942Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/fa33eaf29a606102c8d9ffa45a386a04c2203d9ad18bf4eef3e20c43ebc8/mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3", size = 12759960, upload-time = "2025-07-14T20:33:42.882Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/3f5a29209f27e739ca57e6350bc6b783a38c7621bdf9cac3ab8a08665801/mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70", size = 9503888, upload-time = "2025-07-14T20:32:34.392Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/e6824ed620bbf51d3bf4d6cbbe4953e83eaf31a448d1b3cfb3620ccb641c/mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb", size = 11086395, upload-time = "2025-07-14T20:34:11.452Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/a4afd1ae279707953be175d303f04a5a7bd7e28dc62463ad29c1c857927e/mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d", size = 10120052, upload-time = "2025-07-14T20:33:09.897Z" }, + { url = "https://files.pythonhosted.org/packages/8a/71/19adfeac926ba8205f1d1466d0d360d07b46486bf64360c54cb5a2bd86a8/mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8", size = 11861806, upload-time = "2025-07-14T20:32:16.028Z" }, + { url = "https://files.pythonhosted.org/packages/0b/64/d6120eca3835baf7179e6797a0b61d6c47e0bc2324b1f6819d8428d5b9ba/mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e", size = 12744371, upload-time = "2025-07-14T20:33:33.503Z" }, + { url = "https://files.pythonhosted.org/packages/1f/dc/56f53b5255a166f5bd0f137eed960e5065f2744509dfe69474ff0ba772a5/mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8", size = 12914558, upload-time = "2025-07-14T20:33:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/070bad311171badc9add2910e7f89271695a25c136de24bbafc7eded56d5/mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d", size = 9585447, upload-time = "2025-07-14T20:32:20.594Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" }, ] [[package]] @@ -1162,30 +1108,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 +1200,6 @@ dependencies = [ { name = "inputs" }, { name = "json-rpc" }, { name = "libusb1" }, - { name = "llvmlite" }, { name = "numpy" }, { name = "onnx" }, { name = "psutil" }, @@ -1300,13 +1254,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 +1270,6 @@ testing = [ ] tools = [ { name = "metadrive-simulator", marker = "platform_machine != 'aarch64'" }, - { name = "rerun-sdk" }, ] [package.metadata] @@ -1331,7 +1282,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 +1292,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 +1315,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 +1420,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]] @@ -1614,32 +1561,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 +1766,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 +1945,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 +1960,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 +1973,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 +1987,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 +2000,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 +2013,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 +2026,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 +2039,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 +2055,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 +2068,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 +2082,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 +2096,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 +2110,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 +2124,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 +2141,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 +2156,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 +2170,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 +2184,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 +2201,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 +2214,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 +2227,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 +2241,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 +2254,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 +2268,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 +2284,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 +2298,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 +2314,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 +2327,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 +2340,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 +2353,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 +2367,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 +2382,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 +2396,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 +2411,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 +2425,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 +2439,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 +2452,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 +2466,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 +2480,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 +2494,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 +2508,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 +2522,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 +2536,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 +2551,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 +2565,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 +2580,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 +2594,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 +2608,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 +2621,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 +2634,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 +2647,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 +2660,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 +2674,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 +2688,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 +2701,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 +2714,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 +2727,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 +2740,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 +2753,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 +2767,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 +2781,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 +2795,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 +2808,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 +2821,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 +2835,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 +2849,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 +2863,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 +2877,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 +2892,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 +2907,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 +2921,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 +2935,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 +2949,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 +2962,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 +2976,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 +2990,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 +3004,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 +3018,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 +3031,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 +3044,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 +3057,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 +3070,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 +3083,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 +3096,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 +3110,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 +3124,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 +3138,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 +3153,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 +3167,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 +3180,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 +3196,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 +3209,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 +3225,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 +3239,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 +3252,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 +3266,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 +3280,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 +3294,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 +3309,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 +3323,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 +3336,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 +3350,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 +3363,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 +3378,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 +3392,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 +3405,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 +3418,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 +3432,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 +3446,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 +3460,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 +3473,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 +3486,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 +3502,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 +3516,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 +3529,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 +3542,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 +3556,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 +3570,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 +3583,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 +3597,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 +3611,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 +3625,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 +3639,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 +3653,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 +3668,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 +3683,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 +3698,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 +3712,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 +3725,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 +3739,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 +3752,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 +3766,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 +3780,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 +3795,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 +3809,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 +3823,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 +3836,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 +3850,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 +3864,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 +3878,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 +3891,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 +3904,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 +3918,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 +3933,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 +3947,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 +3960,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 +3975,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 +3989,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 +4003,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 +4016,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 +4029,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 +4043,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 +4057,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 +4070,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 +4086,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 +4100,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 +4116,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 +4223,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 +4308,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 +4332,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 +4367,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]] @@ -4628,25 +4535,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" @@ -4696,27 +4584,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.0" +version = "0.12.7" 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/a1/81/0bd3594fa0f690466e41bd033bdcdf86cba8288345ac77ad4afbe5ec743a/ruff-0.12.7.tar.gz", hash = "sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71", size = 5197814, upload-time = "2025-07-29T22:32:35.877Z" } 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/e1/d2/6cb35e9c85e7a91e8d22ab32ae07ac39cc34a71f1009a6f9e4a2a019e602/ruff-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303", size = 11852189, upload-time = "2025-07-29T22:31:41.281Z" }, + { url = "https://files.pythonhosted.org/packages/63/5b/a4136b9921aa84638f1a6be7fb086f8cad0fde538ba76bda3682f2599a2f/ruff-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb", size = 12519389, upload-time = "2025-07-29T22:31:54.265Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c9/3e24a8472484269b6b1821794141f879c54645a111ded4b6f58f9ab0705f/ruff-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3", size = 11743384, upload-time = "2025-07-29T22:31:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/7c/458dd25deeb3452c43eaee853c0b17a1e84169f8021a26d500ead77964fd/ruff-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860", size = 11943759, upload-time = "2025-07-29T22:32:01.95Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8b/658798472ef260ca050e400ab96ef7e85c366c39cf3dfbef4d0a46a528b6/ruff-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c", size = 11654028, upload-time = "2025-07-29T22:32:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/a8/86/9c2336f13b2a3326d06d39178fd3448dcc7025f82514d1b15816fe42bfe8/ruff-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423", size = 13225209, upload-time = "2025-07-29T22:32:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/76/69/df73f65f53d6c463b19b6b312fd2391dc36425d926ec237a7ed028a90fc1/ruff-0.12.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb", size = 14182353, upload-time = "2025-07-29T22:32:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/58/1e/de6cda406d99fea84b66811c189b5ea139814b98125b052424b55d28a41c/ruff-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd", size = 13631555, upload-time = "2025-07-29T22:32:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ae/625d46d5164a6cc9261945a5e89df24457dc8262539ace3ac36c40f0b51e/ruff-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e", size = 12667556, upload-time = "2025-07-29T22:32:15.312Z" }, + { url = "https://files.pythonhosted.org/packages/55/bf/9cb1ea5e3066779e42ade8d0cd3d3b0582a5720a814ae1586f85014656b6/ruff-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606", size = 12939784, upload-time = "2025-07-29T22:32:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/55/7f/7ead2663be5627c04be83754c4f3096603bf5e99ed856c7cd29618c691bd/ruff-0.12.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8", size = 11771356, upload-time = "2025-07-29T22:32:20.134Z" }, + { url = "https://files.pythonhosted.org/packages/17/40/a95352ea16edf78cd3a938085dccc55df692a4d8ba1b3af7accbe2c806b0/ruff-0.12.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa", size = 11612124, upload-time = "2025-07-29T22:32:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/633b04871c669e23b8917877e812376827c06df866e1677f15abfadc95cb/ruff-0.12.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5", size = 12479945, upload-time = "2025-07-29T22:32:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/be/34/c3ef2d7799c9778b835a76189c6f53c179d3bdebc8c65288c29032e03613/ruff-0.12.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4", size = 12998677, upload-time = "2025-07-29T22:32:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/77/ab/aca2e756ad7b09b3d662a41773f3edcbd262872a4fc81f920dc1ffa44541/ruff-0.12.7-py3-none-win32.whl", hash = "sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77", size = 11756687, upload-time = "2025-07-29T22:32:29.381Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/26d45a5042bc71db22ddd8252ca9d01e9ca454f230e2996bb04f16d72799/ruff-0.12.7-py3-none-win_amd64.whl", hash = "sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f", size = 12912365, upload-time = "2025-07-29T22:32:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9b/0b8aa09817b63e78d94b4977f18b1fcaead3165a5ee49251c5d5c245bb2d/ruff-0.12.7-py3-none-win_arm64.whl", hash = "sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69", size = 11982083, upload-time = "2025-07-29T22:32:33.881Z" }, ] [[package]] @@ -4730,15 +4618,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.30.0" +version = "2.34.0" 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/b3/05/546f8b9baa303b9e9b38feab79222935d0a279f0ed4d2e2cb6e5a0963055/sentry_sdk-2.34.0.tar.gz", hash = "sha256:a024baf3bb229d4b482cb58e9755c212a157813a655f186060533e75a72240ea", size = 336952, upload-time = "2025-07-29T12:47:26.59Z" } 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/24/d4/999d63debd1e53f18c95861ae425dcd4fca1e8ec1934c5906ca8da44e867/sentry_sdk-2.34.0-py2.py3-none-any.whl", hash = "sha256:1c9856d0666c112f3a7a749aba09821e79871b3e7d322833840e9358b8c71a60", size = 357707, upload-time = "2025-07-29T12:47:24.599Z" }, ] [[package]] @@ -4898,35 +4786,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" @@ -4962,11 +4821,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 +4872,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]]