diff --git a/.github/workflows/auto_pr_review.yaml b/.github/workflows/auto_pr_review.yaml index cedeee1741..edb058d7d1 100644 --- a/.github/workflows/auto_pr_review.yaml +++ b/.github/workflows/auto_pr_review.yaml @@ -12,12 +12,12 @@ jobs: issues: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: false # Label PRs - - uses: actions/labeler@v5.0.0 + - uses: actions/labeler@v6 with: dot: true configuration-path: .github/labeler.yaml @@ -55,13 +55,13 @@ jobs: repo: context.repo.repo, issue_number: prNumber }); - + const hasDevC3Label = labels.some(label => label.name === process.env.PR_LABEL); const hasTrustLabel = labels.some(label => label.name === process.env.TRUST_FORK_PR_LABEL); - + console.log(`PR #${prNumber} has ${process.env.PR_LABEL} label: ${hasDevC3Label}`); console.log(`PR #${prNumber} has ${process.env.TRUST_FORK_PR_LABEL} label: ${hasTrustLabel}`); - + core.setOutput('has-dev', hasDevC3Label ? 'true' : 'false'); core.setOutput('has-trust', hasTrustLabel ? 'true' : 'false'); @@ -81,7 +81,7 @@ jobs: }); console.log(`Removed '${process.env.TRUST_FORK_PR_LABEL}' label from PR #${prNumber} as it received new commits`); - + // Add a comment to the PR await github.rest.issues.createComment({ owner: context.repo.owner, diff --git a/.github/workflows/badges.yaml b/.github/workflows/badges.yaml index 6cd7a4687b..2d9ba46d0a 100644 --- a/.github/workflows/badges.yaml +++ b/.github/workflows/badges.yaml @@ -17,7 +17,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/workflows/setup-with-retry diff --git a/.github/workflows/build-all-tinygrad-models.yaml b/.github/workflows/build-all-tinygrad-models.yaml index 00864d38a0..e901baee0c 100644 --- a/.github/workflows/build-all-tinygrad-models.yaml +++ b/.github/workflows/build-all-tinygrad-models.yaml @@ -140,7 +140,7 @@ jobs: 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}' + find output -maxdepth 1 -name 'model-*' -printf "%f\n" | 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 @@ -168,6 +168,7 @@ jobs: 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: + fail-fast: false max-parallel: 1 matrix: model: ${{ fromJson(needs.setup.outputs.model_matrix) }} diff --git a/.github/workflows/ci_weekly_report.yaml b/.github/workflows/ci_weekly_report.yaml index 1d3563fb18..eea3cb3f4d 100644 --- a/.github/workflows/ci_weekly_report.yaml +++ b/.github/workflows/ci_weekly_report.yaml @@ -41,7 +41,7 @@ jobs: if: always() && github.repository == 'commaai/openpilot' steps: - name: Get job results - uses: actions/github-script@v7 + uses: actions/github-script@v8 id: get-job-results with: script: | diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index ce225034d9..27d36f9a4e 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -22,7 +22,7 @@ jobs: steps: - uses: commaai/timeout@v1 - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true @@ -34,7 +34,7 @@ jobs: mkdocs build # Push to docs.comma.ai - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 if: github.ref == 'refs/heads/master' && github.repository == 'sunnypilot/sunnypilot' with: path: openpilot-docs diff --git a/.github/workflows/jenkins-pr-trigger.yaml b/.github/workflows/jenkins-pr-trigger.yaml index 14e2fdf49b..fdd26253fa 100644 --- a/.github/workflows/jenkins-pr-trigger.yaml +++ b/.github/workflows/jenkins-pr-trigger.yaml @@ -5,7 +5,44 @@ on: types: [created, edited] jobs: - # TODO: gc old branches in a separate job in this workflow + cleanup-branches: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Delete stale Jenkins branches + uses: actions/github-script@v8 + with: + script: | + const cutoff = Date.now() - 24 * 60 * 60 * 1000; + const prefixes = ['tmp-jenkins', '__jenkins']; + + for await (const response of github.paginate.iterator(github.rest.repos.listBranches, { + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100, + })) { + for (const branch of response.data) { + if (!prefixes.some(p => branch.name.startsWith(p))) continue; + + const { data: commit } = await github.rest.repos.getCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: branch.commit.sha, + }); + + const commitDate = new Date(commit.commit.committer.date).getTime(); + if (commitDate < cutoff) { + console.log(`Deleting branch: ${branch.name} (last commit: ${commit.commit.committer.date})`); + await github.rest.git.deleteRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `heads/${branch.name}`, + }); + } + } + } + scan-comments: runs-on: ubuntu-latest if: ${{ github.event.issue.pull_request }} @@ -15,7 +52,7 @@ jobs: steps: - name: Check for trigger phrase id: check_comment - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const triggerPhrase = "trigger-jenkins"; @@ -35,7 +72,7 @@ jobs: - name: Checkout repository if: steps.check_comment.outputs.result == 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: refs/pull/${{ github.event.issue.number }}/head @@ -49,7 +86,7 @@ jobs: - name: Delete trigger comment if: steps.check_comment.outputs.result == 'true' && always() - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | await github.rest.issues.deleteComment({ diff --git a/.github/workflows/mici_raylib_ui_preview.yaml b/.github/workflows/mici_raylib_ui_preview.yaml index 5552586529..fa51666bb6 100644 --- a/.github/workflows/mici_raylib_ui_preview.yaml +++ b/.github/workflows/mici_raylib_ui_preview.yaml @@ -33,7 +33,7 @@ jobs: pull-requests: write actions: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true @@ -62,7 +62,7 @@ jobs: path: ${{ github.workspace }}/pr_ui - name: Getting master ui # filename: master_ui_raylib/mici_ui_replay.mp4 - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: sunnypilot/ci-artifacts ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} diff --git a/.github/workflows/model_review.yaml b/.github/workflows/model_review.yaml index 0e1825864c..6b8ce143db 100644 --- a/.github/workflows/model_review.yaml +++ b/.github/workflows/model_review.yaml @@ -16,9 +16,9 @@ jobs: if: github.repository == 'commaai/openpilot' steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Checkout master - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: master path: base diff --git a/.github/workflows/prebuilt.yaml b/.github/workflows/prebuilt.yaml index dde290d5c5..a038cdce63 100644 --- a/.github/workflows/prebuilt.yaml +++ b/.github/workflows/prebuilt.yaml @@ -29,7 +29,7 @@ jobs: running-workflow-name: 'build prebuilt' repo-token: ${{ secrets.GITHUB_TOKEN }} check-regexp: ^((?!.*(build master-ci).*).)*$ - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - run: git lfs pull diff --git a/.github/workflows/raylib_ui_preview.yaml b/.github/workflows/raylib_ui_preview.yaml index 99651a231e..7f569f3bf1 100644 --- a/.github/workflows/raylib_ui_preview.yaml +++ b/.github/workflows/raylib_ui_preview.yaml @@ -58,7 +58,7 @@ jobs: path: ${{ github.workspace }}/pr_ui - name: Getting master ui - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: sunnypilot/ci-artifacts ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b9303045d0..3a6cfa59d0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -30,7 +30,7 @@ jobs: running-workflow-name: 'build __nightly' repo-token: ${{ secrets.GITHUB_TOKEN }} check-regexp: ^((?!.*(build prebuilt).*).)*$ - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true fetch-depth: 0 diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index 49dae93747..9be42cd041 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -8,20 +8,20 @@ on: env: 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 + RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c jobs: update_translations: runs-on: ubuntu-latest if: github.repository == 'sunnypilot/sunnypilot' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: ./.github/workflows/setup-with-retry - name: Update translations run: | ${{ env.RUN }} "python3 selfdrive/ui/update_translations.py --vanish" - name: Create Pull Request - uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 with: author: github-actions[bot] commit-message: "Update translations" @@ -39,18 +39,26 @@ jobs: image: ghcr.io/sunnypilot/sunnypilot-base:latest if: github.repository == 'sunnypilot/sunnypilot' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: uv lock - if: github.repository == 'commaai/openpilot' run: | python3 -m ensurepip --upgrade pip3 install uv uv lock --upgrade + - name: uv pip tree + id: pip_tree + run: | + echo 'PIP_TREE<> $GITHUB_OUTPUT + uv pip tree >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT - name: bump submodules run: | git config --global --add safe.directory '*' + git config submodule.msgq.update none + git config submodule.rednose_repo.update none + git config submodule.teleoprtc_repo.update none git config submodule.tinygrad.update none git submodule update --remote git add . @@ -61,7 +69,7 @@ jobs: python selfdrive/car/docs.py git add docs/CARS.md - name: Create Pull Request - uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 with: author: github-actions[bot] token: ${{ github.repository == 'commaai/openpilot' && secrets.ACTIONS_CREATE_PR_PAT || secrets.GITHUB_TOKEN }} @@ -70,5 +78,10 @@ jobs: branch: auto-package-updates base: master delete-branch: true - body: 'Automatic PR from repo-maintenance -> package_updates' + body: | + Automatic PR from repo-maintenance -> package_updates + + ``` + ${{ steps.pip_tree.outputs.PIP_TREE }} + ``` labels: bot diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 38bdb198d6..dab61bf3b4 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -13,7 +13,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@v10 with: exempt-all-milestones: true @@ -34,7 +34,7 @@ jobs: stale_drafts: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@v10 with: exempt-all-milestones: true diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7ae393bb5b..5cd51f9443 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -20,12 +20,10 @@ concurrency: env: PYTHONWARNINGS: error 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: 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 + RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical @@ -41,7 +39,7 @@ jobs: env: STRIPPED_DIR: /tmp/releasepilot steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Getting LFS files @@ -93,7 +91,7 @@ jobs: && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Setup docker push @@ -107,9 +105,10 @@ jobs: build_mac: name: build macOS + if: false # tmp disable due to brew install not working 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 + - uses: actions/checkout@v6 with: submodules: true - run: echo "CACHE_COMMIT_DATE=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d-%H:%M')" >> $GITHUB_ENV @@ -163,7 +162,7 @@ jobs: env: PYTHONWARNINGS: default steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/workflows/setup-with-retry @@ -180,7 +179,7 @@ jobs: && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/workflows/setup-with-retry @@ -206,22 +205,23 @@ jobs: && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/workflows/setup-with-retry id: setup-step - name: Cache test routes id: dependency-cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: .ci_cache/comma_download_cache - key: proc-replay-${{ hashFiles('selfdrive/test/process_replay/ref_commit', 'selfdrive/test/process_replay/test_processes.py') }} + key: proc-replay-${{ hashFiles('selfdrive/test/process_replay/test_processes.py') }} - name: Build openpilot run: | ${{ env.RUN }} "scons -j$(nproc)" - name: Run replay timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }} + continue-on-error: ${{ github.ref == 'refs/heads/master' }} run: | ${{ env.RUN }} "selfdrive/test/process_replay/test_processes.py -j$(nproc) && \ chmod -R 777 /tmp/comma_download_cache" @@ -229,16 +229,32 @@ jobs: id: print-diff if: always() run: cat selfdrive/test/process_replay/diff.txt - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 if: always() continue-on-error: true with: name: process_replay_diff.txt path: selfdrive/test/process_replay/diff.txt - - name: Upload reference logs - if: false # TODO: move this to github instead of azure + - name: Checkout ci-artifacts + if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master' + uses: actions/checkout@v4 + with: + repository: commaai/ci-artifacts + ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} + path: ${{ github.workspace }}/ci-artifacts + - name: Push refs + if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master' + working-directory: ${{ github.workspace }}/ci-artifacts run: | - ${{ env.RUN }} "unset PYTHONWARNINGS && AZURE_TOKEN='$AZURE_TOKEN' python3 selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only" + git checkout --orphan process-replay + git rm -rf . + git config user.name "GitHub Actions Bot" + git config user.email "<>" + cp ${{ github.workspace }}/selfdrive/test/process_replay/fakedata/*.zst . + echo "${{ github.sha }}" > ref_commit + git add . + git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}" + git push origin process-replay --force - name: Run regen if: false timeout-minutes: 4 @@ -256,7 +272,7 @@ jobs: || fromJSON('["ubuntu-24.04"]') }} if: false # FIXME: Started to timeout recently steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/workflows/setup-with-retry @@ -280,7 +296,7 @@ jobs: && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/workflows/setup-with-retry @@ -292,7 +308,7 @@ jobs: source selfdrive/test/setup_xvfb.sh && python3 selfdrive/ui/tests/test_ui/raylib_screenshots.py" - name: Upload Raylib UI Report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} path: selfdrive/ui/tests/test_ui/raylib_report/screenshots @@ -306,7 +322,7 @@ jobs: && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/workflows/setup-with-retry @@ -318,7 +334,7 @@ jobs: source selfdrive/test/setup_xvfb.sh && WINDOWED=1 python3 selfdrive/ui/tests/diff/replay.py" - name: Upload Raylib UI Report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: mici-raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} path: selfdrive/ui/tests/diff/report diff --git a/.github/workflows/vendor_third_party.yaml b/.github/workflows/vendor_third_party.yaml new file mode 100644 index 0000000000..df50cfad23 --- /dev/null +++ b/.github/workflows/vendor_third_party.yaml @@ -0,0 +1,51 @@ +name: vendor third_party + +on: + workflow_dispatch: + +jobs: + build: + if: github.ref != 'refs/heads/master' + strategy: + matrix: + os: [ubuntu-24.04, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v6 + with: + submodules: true + - name: Build + run: third_party/build.sh + - name: Package artifacts + run: | + git add -A third_party/ + git diff --cached --name-only -- third_party/ | tar -cf /tmp/third_party_build.tar -T - + - uses: actions/upload-artifact@v4 + with: + name: third-party-${{ runner.os }} + path: /tmp/third_party_build.tar + + commit: + needs: build + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + - uses: actions/download-artifact@v4 + with: + path: /tmp/artifacts + - name: Commit vendored libraries + run: | + for f in /tmp/artifacts/*/third_party_build.tar; do + tar xf "$f" + done + git add third_party/ + if git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git commit -m "third_party: rebuild vendor libraries" + git push diff --git a/.gitignore b/.gitignore index b06d620d4e..18f494148d 100644 --- a/.gitignore +++ b/.gitignore @@ -99,6 +99,11 @@ Pipfile .history .ionide +.claude/ +.context/ +PLAN.md +TASK.md + ### JetBrains ### !.idea/customTargets.xml !.idea/tools/* diff --git a/.gitmodules b/.gitmodules index cd6cf2168f..5c5d72a7dc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,7 +15,7 @@ url = https://github.com/commaai/teleoprtc [submodule "tinygrad"] path = tinygrad_repo - url = https://github.com/commaai/tinygrad.git + url = https://github.com/sunnypilot/tinygrad.git [submodule "sunnypilot/neural_network_data"] path = sunnypilot/neural_network_data url = https://github.com/sunnypilot/neural-network-data.git diff --git a/Dockerfile.sunnypilot b/Dockerfile.sunnypilot index 88a226ad08..abc207171e 100644 --- a/Dockerfile.sunnypilot +++ b/Dockerfile.sunnypilot @@ -9,4 +9,6 @@ WORKDIR ${OPENPILOT_PATH} COPY . ${OPENPILOT_PATH}/ -RUN scons --cache-readonly -j$(nproc) +ENV UV_BIN="/home/batman/.local/bin/" +ENV PATH="$UV_BIN:$PATH" +RUN UV_PROJECT_ENVIRONMENT=$VIRTUAL_ENV uv run scons --cache-readonly -j$(nproc) diff --git a/SConstruct b/SConstruct index b2abae8d1b..9f54e2a560 100644 --- a/SConstruct +++ b/SConstruct @@ -14,7 +14,6 @@ Decider('MD5-timestamp') SetOption('num_jobs', max(1, int(os.cpu_count()/2))) -AddOption('--kaitai', action='store_true', help='Regenerate kaitai struct parsers') AddOption('--asan', action='store_true', help='turn on ASAN') AddOption('--ubsan', action='store_true', help='turn on UBSan') AddOption('--mutation', action='store_true', help='generate mutation-ready code') @@ -203,7 +202,6 @@ SConscript(['rednose/SConscript']) # Build system services SConscript([ - 'system/ubloxd/SConscript', 'system/loggerd/SConscript', ]) diff --git a/cereal/SConscript b/cereal/SConscript index a58a9490ce..73dc61844b 100644 --- a/cereal/SConscript +++ b/cereal/SConscript @@ -13,7 +13,7 @@ cereal = env.Library('cereal', [f'gen/cpp/{s}.c++' for s in schema_files]) # Build messaging services_h = env.Command(['services.h'], ['services.py'], 'python3 ' + cereal_dir.path + '/services.py > $TARGET') -env.Program('messaging/bridge', ['messaging/bridge.cc', 'messaging/msgq_to_zmq.cc'], LIBS=[msgq, common, 'pthread']) +env.Program('messaging/bridge', ['messaging/bridge.cc', 'messaging/msgq_to_zmq.cc', 'messaging/bridge_zmq.cc'], LIBS=[msgq, common, 'pthread']) socketmaster = env.Library('socketmaster', ['messaging/socketmaster.cc']) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 5c0a004fa6..53986262ec 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -153,6 +153,7 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { navigation @1; vision @2; policy @3; + offPolicy @4; } } diff --git a/cereal/log.capnp b/cereal/log.capnp index b2a9a789e6..343b7a97fa 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -1478,6 +1478,11 @@ struct ProcLog { cmdline @15 :List(Text); exe @16 :Text; + + # from /proc//smaps_rollup (proportional/private memory) + memPss @17 :UInt64; # Pss — shared pages split by mapper count + memPssAnon @18 :UInt64; # Pss_Anon — private anonymous (heap, stack) + memPssShmem @19 :UInt64; # Pss_Shmem — proportional MSGQ/tmpfs share } struct CPUTimes { diff --git a/cereal/messaging/__init__.py b/cereal/messaging/__init__.py index d5033cd634..2c925b4cc4 100644 --- a/cereal/messaging/__init__.py +++ b/cereal/messaging/__init__.py @@ -1,10 +1,8 @@ # must be built with scons -from msgq.ipc_pyx import Context, Poller, SubSocket, PubSocket, SocketEventHandle, toggle_fake_events, \ - set_fake_prefix, get_fake_prefix, delete_fake_prefix, wait_for_one_event -from msgq.ipc_pyx import MultiplePublishersError, IpcError -from msgq import fake_event_handle, drain_sock_raw +from msgq import fake_event_handle, drain_sock_raw, MultiplePublishersError, IpcError, \ + Context, Poller, SubSocket, PubSocket, SocketEventHandle, toggle_fake_events, \ + set_fake_prefix, get_fake_prefix, delete_fake_prefix, wait_for_one_event import msgq - import os import capnp import time diff --git a/cereal/messaging/bridge.cc b/cereal/messaging/bridge.cc index 69ecd188e1..77823c413a 100644 --- a/cereal/messaging/bridge.cc +++ b/cereal/messaging/bridge.cc @@ -25,15 +25,16 @@ void msgq_to_zmq(const std::vector &endpoints, const std::string &i } void zmq_to_msgq(const std::vector &endpoints, const std::string &ip) { - auto poller = std::make_unique(); - auto pub_context = std::make_unique(); - auto sub_context = std::make_unique(); - std::map sub2pub; + auto poller = std::make_unique(); + auto pub_context = std::make_unique(); + auto sub_context = std::make_unique(); + std::map sub2pub; for (auto endpoint : endpoints) { - auto pub_sock = new MSGQPubSocket(); - auto sub_sock = new ZMQSubSocket(); - pub_sock->connect(pub_context.get(), endpoint); + auto pub_sock = new PubSocket(); + auto sub_sock = new BridgeZmqSubSocket(); + size_t queue_size = services.at(endpoint).queue_size; + pub_sock->connect(pub_context.get(), endpoint, true, queue_size); sub_sock->connect(sub_context.get(), endpoint, ip, false); poller->registerSocket(sub_sock); diff --git a/cereal/messaging/bridge_zmq.cc b/cereal/messaging/bridge_zmq.cc new file mode 100644 index 0000000000..5c56673b47 --- /dev/null +++ b/cereal/messaging/bridge_zmq.cc @@ -0,0 +1,170 @@ +#include "cereal/messaging/bridge_zmq.h" + +#include +#include +#include + +static size_t fnv1a_hash(const std::string &str) { + const size_t fnv_prime = 0x100000001b3; + size_t hash_value = 0xcbf29ce484222325; + for (char c : str) { + hash_value ^= (unsigned char)c; + hash_value *= fnv_prime; + } + return hash_value; +} + +// FIXME: This is a hack to get the port number from the socket name, might have collisions. +static int get_port(std::string endpoint) { + size_t hash_value = fnv1a_hash(endpoint); + int start_port = 8023; + int max_port = 65535; + return start_port + (hash_value % (max_port - start_port)); +} + +BridgeZmqContext::BridgeZmqContext() { + context = zmq_ctx_new(); +} + +BridgeZmqContext::~BridgeZmqContext() { + if (context != nullptr) { + zmq_ctx_term(context); + } +} + +void BridgeZmqMessage::init(size_t sz) { + size = sz; + data = new char[size]; +} + +void BridgeZmqMessage::init(char *d, size_t sz) { + size = sz; + data = new char[size]; + memcpy(data, d, size); +} + +void BridgeZmqMessage::close() { + if (size > 0) { + delete[] data; + } + data = nullptr; + size = 0; +} + +BridgeZmqMessage::~BridgeZmqMessage() { + close(); +} + +int BridgeZmqSubSocket::connect(BridgeZmqContext *context, std::string endpoint, std::string address, bool conflate, bool check_endpoint) { + sock = zmq_socket(context->getRawContext(), ZMQ_SUB); + if (sock == nullptr) { + return -1; + } + + zmq_setsockopt(sock, ZMQ_SUBSCRIBE, "", 0); + + if (conflate) { + int arg = 1; + zmq_setsockopt(sock, ZMQ_CONFLATE, &arg, sizeof(int)); + } + + int reconnect_ivl = 500; + zmq_setsockopt(sock, ZMQ_RECONNECT_IVL_MAX, &reconnect_ivl, sizeof(reconnect_ivl)); + + full_endpoint = "tcp://" + address + ":"; + if (check_endpoint) { + full_endpoint += std::to_string(get_port(endpoint)); + } else { + full_endpoint += endpoint; + } + + return zmq_connect(sock, full_endpoint.c_str()); +} + +void BridgeZmqSubSocket::setTimeout(int timeout) { + zmq_setsockopt(sock, ZMQ_RCVTIMEO, &timeout, sizeof(int)); +} + +Message *BridgeZmqSubSocket::receive(bool non_blocking) { + zmq_msg_t msg; + assert(zmq_msg_init(&msg) == 0); + + int flags = non_blocking ? ZMQ_DONTWAIT : 0; + int rc = zmq_msg_recv(&msg, sock, flags); + + Message *ret = nullptr; + if (rc >= 0) { + ret = new BridgeZmqMessage; + ret->init((char *)zmq_msg_data(&msg), zmq_msg_size(&msg)); + } + + zmq_msg_close(&msg); + return ret; +} + +BridgeZmqSubSocket::~BridgeZmqSubSocket() { + if (sock != nullptr) { + zmq_close(sock); + } +} + +int BridgeZmqPubSocket::connect(BridgeZmqContext *context, std::string endpoint, bool check_endpoint) { + sock = zmq_socket(context->getRawContext(), ZMQ_PUB); + if (sock == nullptr) { + return -1; + } + + full_endpoint = "tcp://*:"; + if (check_endpoint) { + full_endpoint += std::to_string(get_port(endpoint)); + } else { + full_endpoint += endpoint; + } + + // ZMQ pub sockets cannot be shared between processes, so we need to ensure pid stays the same. + pid = getpid(); + + return zmq_bind(sock, full_endpoint.c_str()); +} + +int BridgeZmqPubSocket::sendMessage(Message *message) { + assert(pid == getpid()); + return zmq_send(sock, message->getData(), message->getSize(), ZMQ_DONTWAIT); +} + +int BridgeZmqPubSocket::send(char *data, size_t size) { + assert(pid == getpid()); + return zmq_send(sock, data, size, ZMQ_DONTWAIT); +} + +BridgeZmqPubSocket::~BridgeZmqPubSocket() { + if (sock != nullptr) { + zmq_close(sock); + } +} + +void BridgeZmqPoller::registerSocket(BridgeZmqSubSocket *socket) { + assert(num_polls + 1 < (sizeof(polls) / sizeof(polls[0]))); + polls[num_polls].socket = socket->getRawSocket(); + polls[num_polls].events = ZMQ_POLLIN; + + sockets.push_back(socket); + num_polls++; +} + +std::vector BridgeZmqPoller::poll(int timeout) { + std::vector ret; + + int rc = zmq_poll(polls, num_polls, timeout); + if (rc < 0) { + return ret; + } + + for (size_t i = 0; i < num_polls; i++) { + if (polls[i].revents) { + ret.push_back(sockets[i]); + } + } + + return ret; +} diff --git a/cereal/messaging/bridge_zmq.h b/cereal/messaging/bridge_zmq.h new file mode 100644 index 0000000000..ebdbc56c24 --- /dev/null +++ b/cereal/messaging/bridge_zmq.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include + +#include + +#include "msgq/ipc.h" + +class BridgeZmqContext { +public: + BridgeZmqContext(); + void *getRawContext() { return context; } + ~BridgeZmqContext(); + +private: + void *context = nullptr; +}; + +class BridgeZmqMessage : public Message { +public: + void init(size_t size); + void init(char *data, size_t size); + void close(); + size_t getSize() { return size; } + char *getData() { return data; } + ~BridgeZmqMessage(); + +private: + char *data = nullptr; + size_t size = 0; +}; + +class BridgeZmqSubSocket { +public: + int connect(BridgeZmqContext *context, std::string endpoint, std::string address, bool conflate = false, bool check_endpoint = true); + void setTimeout(int timeout); + Message *receive(bool non_blocking = false); + void *getRawSocket() { return sock; } + ~BridgeZmqSubSocket(); + +private: + void *sock = nullptr; + std::string full_endpoint; +}; + +class BridgeZmqPubSocket { +public: + int connect(BridgeZmqContext *context, std::string endpoint, bool check_endpoint = true); + int sendMessage(Message *message); + int send(char *data, size_t size); + void *getRawSocket() { return sock; } + ~BridgeZmqPubSocket(); + +private: + void *sock = nullptr; + std::string full_endpoint; + int pid = -1; +}; + +class BridgeZmqPoller { +public: + void registerSocket(BridgeZmqSubSocket *socket); + std::vector poll(int timeout); + +private: + static constexpr size_t MAX_BRIDGE_ZMQ_POLLERS = 128; + std::vector sockets; + zmq_pollitem_t polls[MAX_BRIDGE_ZMQ_POLLERS] = {}; + size_t num_polls = 0; +}; diff --git a/cereal/messaging/msgq_to_zmq.cc b/cereal/messaging/msgq_to_zmq.cc index ce626f2aad..5e7ea22273 100644 --- a/cereal/messaging/msgq_to_zmq.cc +++ b/cereal/messaging/msgq_to_zmq.cc @@ -2,6 +2,7 @@ #include +#include "cereal/services.h" #include "common/util.h" extern ExitHandler do_exit; @@ -21,14 +22,14 @@ static std::string recv_zmq_msg(void *sock) { } void MsgqToZmq::run(const std::vector &endpoints, const std::string &ip) { - zmq_context = std::make_unique(); - msgq_context = std::make_unique(); + zmq_context = std::make_unique(); + msgq_context = std::make_unique(); // Create ZMQPubSockets for each endpoint for (const auto &endpoint : endpoints) { auto &socket_pair = socket_pairs.emplace_back(); socket_pair.endpoint = endpoint; - socket_pair.pub_sock = std::make_unique(); + socket_pair.pub_sock = std::make_unique(); int ret = socket_pair.pub_sock->connect(zmq_context.get(), endpoint); if (ret != 0) { printf("Failed to create ZMQ publisher for [%s]: %s\n", endpoint.c_str(), zmq_strerror(zmq_errno())); @@ -48,7 +49,7 @@ void MsgqToZmq::run(const std::vector &endpoints, const std::string for (auto sub_sock : msgq_poller->poll(100)) { // Process messages for each socket - ZMQPubSocket *pub_sock = sub2pub.at(sub_sock); + BridgeZmqPubSocket *pub_sock = sub2pub.at(sub_sock); for (int i = 0; i < MAX_MESSAGES_PER_SOCKET; ++i) { auto msg = std::unique_ptr(sub_sock->receive(true)); if (!msg) break; @@ -71,7 +72,7 @@ void MsgqToZmq::zmqMonitorThread() { // Set up ZMQ monitor for each pub socket for (int i = 0; i < socket_pairs.size(); ++i) { std::string addr = "inproc://op-bridge-monitor-" + std::to_string(i); - zmq_socket_monitor(socket_pairs[i].pub_sock->sock, addr.c_str(), ZMQ_EVENT_ACCEPTED | ZMQ_EVENT_DISCONNECTED); + zmq_socket_monitor(socket_pairs[i].pub_sock->getRawSocket(), addr.c_str(), ZMQ_EVENT_ACCEPTED | ZMQ_EVENT_DISCONNECTED); void *monitor_socket = zmq_socket(zmq_context->getRawContext(), ZMQ_PAIR); zmq_connect(monitor_socket, addr.c_str()); @@ -108,7 +109,8 @@ void MsgqToZmq::zmqMonitorThread() { if (++pair.connected_clients == 1) { // Create new MSGQ subscriber socket and map to ZMQ publisher pair.sub_sock = std::make_unique(); - pair.sub_sock->connect(msgq_context.get(), pair.endpoint, "127.0.0.1"); + size_t queue_size = services.at(pair.endpoint).queue_size; + pair.sub_sock->connect(msgq_context.get(), pair.endpoint, "127.0.0.1", false, true, queue_size); sub2pub[pair.sub_sock.get()] = pair.pub_sock.get(); registerSockets(); } @@ -128,7 +130,7 @@ void MsgqToZmq::zmqMonitorThread() { // Clean up monitor sockets for (int i = 0; i < pollitems.size(); ++i) { - zmq_socket_monitor(socket_pairs[i].pub_sock->sock, nullptr, 0); + zmq_socket_monitor(socket_pairs[i].pub_sock->getRawSocket(), nullptr, 0); zmq_close(pollitems[i].socket); } cv.notify_one(); diff --git a/cereal/messaging/msgq_to_zmq.h b/cereal/messaging/msgq_to_zmq.h index ebdbe5df69..64f3a2173e 100644 --- a/cereal/messaging/msgq_to_zmq.h +++ b/cereal/messaging/msgq_to_zmq.h @@ -7,9 +7,8 @@ #include #include -#define private public #include "msgq/impl_msgq.h" -#include "msgq/impl_zmq.h" +#include "cereal/messaging/bridge_zmq.h" class MsgqToZmq { public: @@ -22,16 +21,16 @@ protected: struct SocketPair { std::string endpoint; - std::unique_ptr pub_sock; + std::unique_ptr pub_sock; std::unique_ptr sub_sock; int connected_clients = 0; }; - std::unique_ptr msgq_context; - std::unique_ptr zmq_context; + std::unique_ptr msgq_context; + std::unique_ptr zmq_context; std::mutex mutex; std::condition_variable cv; std::unique_ptr msgq_poller; - std::map sub2pub; + std::map sub2pub; std::vector socket_pairs; }; diff --git a/common/i2c.py b/common/i2c.py new file mode 100644 index 0000000000..1dfaa659ad --- /dev/null +++ b/common/i2c.py @@ -0,0 +1,81 @@ +import os +import fcntl +import ctypes + +# I2C constants from /usr/include/linux/i2c-dev.h +I2C_SLAVE = 0x0703 +I2C_SLAVE_FORCE = 0x0706 +I2C_SMBUS = 0x0720 + +# SMBus transfer types +I2C_SMBUS_READ = 1 +I2C_SMBUS_WRITE = 0 +I2C_SMBUS_BYTE_DATA = 2 +I2C_SMBUS_I2C_BLOCK_DATA = 8 + +I2C_SMBUS_BLOCK_MAX = 32 + + +class _I2cSmbusData(ctypes.Union): + _fields_ = [ + ("byte", ctypes.c_uint8), + ("word", ctypes.c_uint16), + ("block", ctypes.c_uint8 * (I2C_SMBUS_BLOCK_MAX + 2)), + ] + + +class _I2cSmbusIoctlData(ctypes.Structure): + _fields_ = [ + ("read_write", ctypes.c_uint8), + ("command", ctypes.c_uint8), + ("size", ctypes.c_uint32), + ("data", ctypes.POINTER(_I2cSmbusData)), + ] + + +class SMBus: + def __init__(self, bus: int): + self._fd = os.open(f'/dev/i2c-{bus}', os.O_RDWR) + + def __enter__(self) -> 'SMBus': + return self + + def __exit__(self, *args) -> None: + self.close() + + def close(self) -> None: + if hasattr(self, '_fd') and self._fd >= 0: + os.close(self._fd) + self._fd = -1 + + def _set_address(self, addr: int, force: bool = False) -> None: + ioctl_arg = I2C_SLAVE_FORCE if force else I2C_SLAVE + fcntl.ioctl(self._fd, ioctl_arg, addr) + + def _smbus_access(self, read_write: int, command: int, size: int, data: _I2cSmbusData) -> None: + ioctl_data = _I2cSmbusIoctlData(read_write, command, size, ctypes.pointer(data)) + fcntl.ioctl(self._fd, I2C_SMBUS, ioctl_data) + + def read_byte_data(self, addr: int, register: int, force: bool = False) -> int: + self._set_address(addr, force) + data = _I2cSmbusData() + self._smbus_access(I2C_SMBUS_READ, register, I2C_SMBUS_BYTE_DATA, data) + return int(data.byte) + + def write_byte_data(self, addr: int, register: int, value: int, force: bool = False) -> None: + self._set_address(addr, force) + data = _I2cSmbusData() + data.byte = value & 0xFF + self._smbus_access(I2C_SMBUS_WRITE, register, I2C_SMBUS_BYTE_DATA, data) + + def read_i2c_block_data(self, addr: int, register: int, length: int, force: bool = False) -> list[int]: + self._set_address(addr, force) + if not (0 <= length <= I2C_SMBUS_BLOCK_MAX): + raise ValueError(f"length must be 0..{I2C_SMBUS_BLOCK_MAX}") + + data = _I2cSmbusData() + data.block[0] = length + self._smbus_access(I2C_SMBUS_READ, register, I2C_SMBUS_I2C_BLOCK_DATA, data) + read_len = int(data.block[0]) or length + read_len = min(read_len, length) + return [int(b) for b in data.block[1 : read_len + 1]] diff --git a/common/model.h b/common/model.h index 156bd00e7f..444727be93 100644 --- a/common/model.h +++ b/common/model.h @@ -1 +1 @@ -#define DEFAULT_MODEL "WMI (Default)" +#define DEFAULT_MODEL "CD210 (Default)" diff --git a/common/params_keys.h b/common/params_keys.h index ecc656cc78..dba0742268 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -115,6 +115,7 @@ inline static std::unordered_map keys = { {"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"SshEnabled", {PERSISTENT | BACKUP, BOOL}}, {"TermsVersion", {PERSISTENT, STRING}}, + {"TorqueBar", {PERSISTENT | BACKUP, BOOL, "0"}}, {"TrainingVersion", {PERSISTENT, STRING}}, {"UbloxAvailable", {PERSISTENT, BOOL}}, {"UpdateAvailable", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, @@ -136,6 +137,7 @@ inline static std::unordered_map keys = { {"ApiCache_DriveStats", {PERSISTENT, JSON}}, {"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}}, {"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}}, + {"BlinkerLateralReengageDelay", {PERSISTENT | BACKUP, INT, "0"}}, // seconds {"BlinkerMinLateralControlSpeed", {PERSISTENT | BACKUP, INT, "20"}}, // MPH or km/h {"BlinkerPauseLateralControl", {PERSISTENT | BACKUP, INT, "0"}}, {"Brightness", {PERSISTENT | BACKUP, INT, "0"}}, @@ -248,7 +250,7 @@ inline static std::unordered_map keys = { {"OsmStateTitle", {PERSISTENT, STRING}}, {"OsmWayTest", {PERSISTENT, STRING}}, {"RoadName", {CLEAR_ON_ONROAD_TRANSITION, STRING}}, - {"RoadNameToggle", {PERSISTENT, STRING}}, + {"RoadNameToggle", {PERSISTENT | BACKUP, BOOL, "0"}}, // Speed Limit {"SpeedLimitMode", {PERSISTENT | BACKUP, INT, "1"}}, diff --git a/common/utils.py b/common/utils.py index caa9a57958..faaa96ecbc 100644 --- a/common/utils.py +++ b/common/utils.py @@ -10,6 +10,27 @@ import zstandard as zstd LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change +class Timer: + """Simple lap timer for profiling sequential operations.""" + + def __init__(self): + self._start = self._lap = time.monotonic() + self._sections = {} + + def lap(self, name): + now = time.monotonic() + self._sections[name] = now - self._lap + self._lap = now + + @property + def total(self): + return time.monotonic() - self._start + + def fmt(self, duration): + parts = ", ".join(f"{k}={v:.2f}s" + (f" ({duration/v:.0f}x)" if k == 'render' and v > 0 else "") for k, v in self._sections.items()) + total = self.total + realtime = f"{duration/total:.1f}x realtime" if total > 0 else "N/A" + return f"{duration}s in {total:.1f}s ({realtime}) | {parts}" def sudo_write(val: str, path: str) -> None: try: @@ -146,6 +167,92 @@ def managed_proc(cmd: list[str], env: dict[str, str]): proc.kill() +def tabulate(tabular_data, headers=(), tablefmt="simple", floatfmt="g", stralign="left", numalign=None): + rows = [list(row) for row in tabular_data] + + def fmt(val): + if isinstance(val, str): + return val + if isinstance(val, (bool, int)): + return str(val) + try: + return format(val, floatfmt) + except (TypeError, ValueError): + return str(val) + + formatted = [[fmt(c) for c in row] for row in rows] + hdrs = [str(h) for h in headers] if headers else None + + ncols = max((len(r) for r in formatted), default=0) + if hdrs: + ncols = max(ncols, len(hdrs)) + if ncols == 0: + return "" + + for r in formatted: + r.extend([""] * (ncols - len(r))) + if hdrs: + hdrs.extend([""] * (ncols - len(hdrs))) + + widths = [0] * ncols + if hdrs: + for i in range(ncols): + widths[i] = len(hdrs[i]) + for row in formatted: + for i in range(ncols): + widths[i] = max(widths[i], max(len(ln) for ln in row[i].split('\n'))) + + def _align(s, w): + if stralign == "center": + return s.center(w) + return s.ljust(w) + + if tablefmt == "html": + parts = [""] + if hdrs: + parts.append("") + parts.append("" + "".join(f"" for h in hdrs) + "") + parts.append("") + parts.append("") + for row in formatted: + parts.append("" + "".join(f"" for c in row) + "") + parts.append("") + parts.append("
{h}
{c}
") + return "\n".join(parts) + + if tablefmt == "simple_grid": + def _sep(left, mid, right): + return left + mid.join("\u2500" * (w + 2) for w in widths) + right + + top, mid_sep, bot = _sep("\u250c", "\u252c", "\u2510"), _sep("\u251c", "\u253c", "\u2524"), _sep("\u2514", "\u2534", "\u2518") + + def _fmt_row(cells): + split = [c.split('\n') for c in cells] + nlines = max(len(s) for s in split) + for s in split: + s.extend([""] * (nlines - len(s))) + return ["\u2502" + "\u2502".join(f" {_align(split[i][li], widths[i])} " for i in range(ncols)) + "\u2502" for li in range(nlines)] + + lines = [top] + if hdrs: + lines.extend(_fmt_row(hdrs)) + lines.append(mid_sep) + for ri, row in enumerate(formatted): + lines.extend(_fmt_row(row)) + lines.append(mid_sep if ri < len(formatted) - 1 else bot) + return "\n".join(lines) + + # simple + gap = " " + lines = [] + if hdrs: + lines.append(gap.join(h.ljust(w) for h, w in zip(hdrs, widths, strict=True))) + lines.append(gap.join("-" * w for w in widths)) + for row in formatted: + lines.append(gap.join(_align(row[i], widths[i]) for i in range(ncols))) + return "\n".join(lines) + + def retry(attempts=3, delay=1.0, ignore_failure=False): def decorator(func): @functools.wraps(func) diff --git a/docs/CARS.md b/docs/CARS.md index 5c659d284e..ab1eabeb13 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,28 +4,26 @@ 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. -# 341 Supported Cars +# 335 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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Acura|MDX 2025|All except Type S|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Acura|MDX 2025-26|All except Type S|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Acura|TLX 2021|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 A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Acura|TLX 2025|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| -|Chevrolet|Bolt EV Non-ACC 2017|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| -|Chevrolet|Bolt EV Non-ACC 2018-21|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| -|Chevrolet|Malibu Non-ACC 2016-23|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -34,33 +32,33 @@ A supported vehicle is one that just works when you install a comma device. All |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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Ford|Focus 2018[2](#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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Focus Hybrid 2018[2](#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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -90,12 +88,11 @@ A supported vehicle is one that just works when you install a comma device. All |Honda|Civic Hatchback Hybrid 2025-26|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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Civic Hybrid 2025-26|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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Honda|Clarity 2018-21|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Nidec connector + Honda Clarity Proxy Board
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|CR-V 2023-26|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Honda|CR-V Hybrid 2023-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|CR-V Hybrid 2023-26|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -106,6 +103,7 @@ A supported vehicle is one that just works when you install a comma device. All |Honda|N-Box 2018|All|openpilot available[1](#footnotes)|0 mph|11 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Odyssey 2021-26|All|openpilot available[1](#footnotes)|0 mph|43 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Odyssey (Taiwan) 2018-19|Honda Sensing|openpilot|19 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Passport 2026|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -120,7 +118,6 @@ A supported vehicle is one that just works when you install a comma device. All |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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Hyundai|Elantra Non-SCC 2022|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Ioniq 5 (Southeast Asia and Europe only) 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 Hyundai Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -138,9 +135,7 @@ A supported vehicle is one that just works when you install a comma device. All |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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Kona Electric (with HDA II, Korea only) 2023|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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Hyundai|Kona Electric Non-SCC 2019|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Hyundai|Kona Non-SCC 2019|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Santa Cruz 2022-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 N connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -164,13 +159,11 @@ A supported vehicle is one that just works when you install a comma device. All |Kia|Carnival 2022-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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|Carnival (China only) 2023|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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Kia|Ceed Plug-in Hybrid Non-SCC 2022|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai I connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|EV6 (Southeast Asia only) 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 Hyundai P connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|EV6 (with HDA II) 2022-24|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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|EV6 (without HDA II) 2022-24|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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Kia|Forte Non-SCC 2019|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|K8 Hybrid (with HDA II) 2023|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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -211,6 +204,7 @@ A supported vehicle is one that just works when you install a comma device. All |Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|LS 2018|All except Lexus Safety System+ A|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|NX 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|NX Hybrid 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -226,21 +220,21 @@ A supported vehicle is one that just works when you install a comma device. All |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 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Nissan[5](#footnotes)|Altima 2019-20, 2024|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 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Nissan[5](#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 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Nissan[5](#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 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Nissan[5](#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 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[5](#footnotes)|Altima 2019-20, 2024|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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[5](#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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[5](#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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[5](#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 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Subaru|Ascent 2019-21|All[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| |Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| |Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| @@ -255,19 +249,19 @@ A supported vehicle is one that just works when you install a comma device. All |Subaru|Outback 2020-22|All[6](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru B connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| |Subaru|XV 2018-19|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| |Subaru|XV 2020-21|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Škoda|Fabia 2022-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| -|Škoda|Kamiq 2021-23[11,13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| -|Škoda|Karoq 2019-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Škoda|Kodiaq 2017-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Škoda|Octavia 2015-19[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Škoda|Octavia RS 2016[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Škoda|Octavia Scout 2017-19[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Škoda|Scala 2020-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| -|Škoda|Superb 2015-22[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Tesla[9](#footnotes)|Model 3 (with HW3) 2019-23[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Tesla[9](#footnotes)|Model 3 (with HW4) 2024-25[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Tesla[9](#footnotes)|Model Y (with HW3) 2020-23[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Tesla[9](#footnotes)|Model Y (with HW4) 2024-25[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Fabia 2022-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Škoda|Kamiq 2021-23[11,13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Škoda|Karoq 2019-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Kodiaq 2017-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Octavia 2015-19[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Octavia RS 2016[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Octavia Scout 2017-19[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Scala 2020-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Škoda|Superb 2015-22[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[9](#footnotes)|Model 3 (with HW3) 2019-23[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla A connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[9](#footnotes)|Model 3 (with HW4) 2024-25[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla B connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[9](#footnotes)|Model Y (with HW3) 2020-23[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla A connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[9](#footnotes)|Model Y (with HW4) 2024-25[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla B connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Avalon 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -313,42 +307,42 @@ A supported vehicle is one that just works when you install a comma device. All |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 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
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 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Sienna 2018-20|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Jetta 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Passat 2015-22[12](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| -|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| -|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| -|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Passat 2015-22[12](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
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/msgq_repo b/msgq_repo index 20f2493855..4c4e814ed5 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit 20f2493855ef32339b80f0ad76b3cb82210dc474 +Subproject commit 4c4e814ed592db52431bb53d37f0bb93299e960c diff --git a/panda b/panda index f9cdec7f7b..a95e060e85 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit f9cdec7f7bf44b5335d8b2345993ae533fbb010b +Subproject commit a95e060e855dc59898dd3bf2274a598dacdd2571 diff --git a/pyproject.toml b/pyproject.toml index 185a9a5222..3d6eb07475 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "openpilot" -requires-python = ">= 3.11, < 3.13" +requires-python = ">= 3.12.3, < 3.13" license = {text = "MIT License"} version = "0.1.0" description = "an open source driver assistance system" authors = [ - {name ="Vehicle Researcher", email="user@comma.ai"} + {name = "Vehicle Researcher", email="user@comma.ai"} ] dependencies = [ @@ -17,9 +17,6 @@ dependencies = [ "crcmod-plus", # cars + qcomgpsd "tqdm", # cars (fw_versions.py) on start + many one-off uses - # hardwared - "smbus2", # configuring amp - # core "cffi", "scons", @@ -36,9 +33,6 @@ dependencies = [ "pyopenssl < 24.3.0", "pyaudio", - # ubloxd (TODO: just use struct) - "kaitaistruct", - # panda "libusb1", "spidev; platform_system == 'Linux'", @@ -58,7 +52,6 @@ dependencies = [ # acados deps "casadi >=3.6.6", # 3.12 fixed in 3.6.6 - "future-fstrings", # joystickd "inputs", @@ -75,12 +68,12 @@ dependencies = [ "raylib > 5.5.0.3", "qrcode", "mapbox-earcut", + "jeepney", ] [project.optional-dependencies] docs = [ "Jinja2", - "natsort", "mkdocs", ] @@ -94,10 +87,8 @@ testing = [ # 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", "pytest-mock", - "pytest-repeat", "ruff", "codespell", "pre-commit-hooks", @@ -105,21 +96,12 @@ testing = [ dev = [ "av", - "azure-identity", - "azure-storage-blob", "dictdiffer", - "jeepney", "matplotlib", "opencv-python-headless", "parameterized >=0.8, <0.9", "pyautogui", - "pyopencl", - "pytools>=2025.1.6; platform_machine != 'aarch64'", "pywinctl", - "pyprof2calltree", - "tabulate", - "types-requests", - "types-tabulate", ] tools = [ @@ -156,19 +138,9 @@ markers = [ testpaths = [ "common", "selfdrive", - "system/manager", - "system/updated", - "system/athena", - "system/camerad", - "system/hardware", - "system/loggerd", - "system/tests", - "system/ubloxd", - "system/webrtc", - "tools/lib/tests", - "tools/replay", - "tools/cabana", - "cereal/messaging/tests", + "system", + "tools", + "cereal", "sunnypilot", ] @@ -179,7 +151,7 @@ ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,w builtin = "clear,rare,informal,code,names,en-GB_to_en-US" skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*, selfdrive/assets/offroad/mici_fcc.html" -# https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml +# https://docs.astral.sh/ruff/configuration/#using-pyprojecttoml [tool.ruff] indent-width = 2 lint.select = [ diff --git a/release/ci/model_generator.py b/release/ci/model_generator.py index ee41343be8..96352254b6 100755 --- a/release/ci/model_generator.py +++ b/release/ci/model_generator.py @@ -68,8 +68,10 @@ def generate_metadata(model_path: Path, output_dir: Path, short_name: str): metadata_file = metadata_file.rename(output_path / f"{base}_{short_name.lower()}_metadata.pkl") # Build the metadata structure + model_type = "offPolicy" if "off_policy" in base else base.split("_")[-1] + model_metadata = { - "type": base.split("_")[-1] if "dmonitoring" not in base else "dmonitoring", + "type": model_type, "artifact": { "file_name": tinygrad_file.name, "download_uri": { diff --git a/scripts/ci_results.py b/scripts/ci_results.py new file mode 100755 index 0000000000..a133541c69 --- /dev/null +++ b/scripts/ci_results.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +"""Fetch CI results from GitHub Actions and Jenkins.""" + +import argparse +import json +import subprocess +import time +import urllib.error +import urllib.request +from datetime import datetime + +JENKINS_URL = "https://jenkins.comma.life" +DEFAULT_TIMEOUT = 1800 # 30 minutes +POLL_INTERVAL = 30 # seconds +LOG_TAIL_LINES = 10 # lines of log to include for failed jobs + + +def get_git_info(): + branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], text=True).strip() + commit = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() + return branch, commit + + +def get_github_actions_status(commit_sha): + result = subprocess.run( + ["gh", "run", "list", "--commit", commit_sha, "--workflow", "tests.yaml", "--json", "databaseId,status,conclusion"], + capture_output=True, text=True, check=True + ) + runs = json.loads(result.stdout) + if not runs: + return None, None + + run_id = runs[0]["databaseId"] + result = subprocess.run( + ["gh", "run", "view", str(run_id), "--json", "jobs"], + capture_output=True, text=True, check=True + ) + data = json.loads(result.stdout) + jobs = {job["name"]: {"status": job["status"], "conclusion": job["conclusion"], + "duration": format_duration(job) if job["conclusion"] not in ("skipped", None) and job.get("startedAt") else "", + "id": job["databaseId"]} + for job in data.get("jobs", [])} + return jobs, run_id + + +def get_github_job_log(run_id, job_id): + result = subprocess.run( + ["gh", "run", "view", str(run_id), "--job", str(job_id), "--log-failed"], + capture_output=True, text=True + ) + lines = result.stdout.strip().split('\n') + return '\n'.join(lines[-LOG_TAIL_LINES:]) if len(lines) > LOG_TAIL_LINES else result.stdout.strip() + + +def format_duration(job): + start = datetime.fromisoformat(job["startedAt"].replace("Z", "+00:00")) + end = datetime.fromisoformat(job["completedAt"].replace("Z", "+00:00")) + secs = int((end - start).total_seconds()) + return f"{secs // 60}m {secs % 60}s" + + +def get_jenkins_status(branch, commit_sha): + base_url = f"{JENKINS_URL}/job/openpilot/job/{branch}" + try: + # Get list of recent builds + with urllib.request.urlopen(f"{base_url}/api/json?tree=builds[number,url]", timeout=10) as resp: + builds = json.loads(resp.read().decode()).get("builds", []) + + # Find build matching commit + for build in builds[:20]: # check last 20 builds + with urllib.request.urlopen(f"{build['url']}api/json", timeout=10) as resp: + data = json.loads(resp.read().decode()) + for action in data.get("actions", []): + if action.get("_class") == "hudson.plugins.git.util.BuildData": + build_sha = action.get("lastBuiltRevision", {}).get("SHA1", "") + if build_sha.startswith(commit_sha) or commit_sha.startswith(build_sha): + # Get stages info + stages = [] + try: + with urllib.request.urlopen(f"{build['url']}wfapi/describe", timeout=10) as resp2: + wf_data = json.loads(resp2.read().decode()) + stages = [{"name": s["name"], "status": s["status"]} for s in wf_data.get("stages", [])] + except urllib.error.HTTPError: + pass + return { + "number": data["number"], + "in_progress": data.get("inProgress", False), + "result": data.get("result"), + "url": data.get("url", ""), + "stages": stages, + } + return None # no build found for this commit + except urllib.error.HTTPError: + return None # branch doesn't exist on Jenkins + + +def get_jenkins_log(build_url): + url = f"{build_url}consoleText" + with urllib.request.urlopen(url, timeout=30) as resp: + text = resp.read().decode(errors='replace') + lines = text.strip().split('\n') + return '\n'.join(lines[-LOG_TAIL_LINES:]) if len(lines) > LOG_TAIL_LINES else text.strip() + + +def is_complete(gh_status, jenkins_status): + gh_done = gh_status is None or all(j["status"] == "completed" for j in gh_status.values()) + jenkins_done = jenkins_status is None or not jenkins_status.get("in_progress", True) + return gh_done and jenkins_done + + +def status_icon(status, conclusion=None): + if status == "completed": + return ":white_check_mark:" if conclusion == "success" else ":x:" + return ":hourglass:" if status == "in_progress" else ":grey_question:" + + +def format_markdown(gh_status, gh_run_id, jenkins_status, commit_sha, branch): + lines = ["# CI Results", "", + f"**Branch**: {branch}", + f"**Commit**: {commit_sha[:7]}", + f"**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", ""] + + lines.extend(["## GitHub Actions", "", "| Job | Status | Duration |", "|-----|--------|----------|"]) + failed_gh_jobs = [] + if gh_status: + for job_name, job in gh_status.items(): + icon = status_icon(job["status"], job.get("conclusion")) + conclusion = job.get("conclusion") or job["status"] + lines.append(f"| {job_name} | {icon} {conclusion} | {job.get('duration', '')} |") + if job.get("conclusion") == "failure": + failed_gh_jobs.append((job_name, job.get("id"))) + else: + lines.append("| - | No workflow runs found | |") + + lines.extend(["", "## Jenkins", "", "| Stage | Status |", "|-------|--------|"]) + failed_jenkins_stages = [] + if jenkins_status: + stages = jenkins_status.get("stages", []) + if stages: + for stage in stages: + icon = ":white_check_mark:" if stage["status"] == "SUCCESS" else ( + ":x:" if stage["status"] == "FAILED" else ":hourglass:") + lines.append(f"| {stage['name']} | {icon} {stage['status'].lower()} |") + if stage["status"] == "FAILED": + failed_jenkins_stages.append(stage["name"]) + # Show overall build status if still in progress + if jenkins_status["in_progress"]: + lines.append("| (build in progress) | :hourglass: in_progress |") + else: + icon = ":hourglass:" if jenkins_status["in_progress"] else ( + ":white_check_mark:" if jenkins_status["result"] == "SUCCESS" else ":x:") + status = "in progress" if jenkins_status["in_progress"] else (jenkins_status["result"] or "unknown") + lines.append(f"| #{jenkins_status['number']} | {icon} {status.lower()} |") + if jenkins_status.get("url"): + lines.append(f"\n[View build]({jenkins_status['url']})") + else: + lines.append("| - | No builds found for branch |") + + if failed_gh_jobs or failed_jenkins_stages: + lines.extend(["", "## Failure Logs", ""]) + + for job_name, job_id in failed_gh_jobs: + lines.append(f"### GitHub Actions: {job_name}") + log = get_github_job_log(gh_run_id, job_id) + lines.extend(["", "```", log, "```", ""]) + + for stage_name in failed_jenkins_stages: + lines.append(f"### Jenkins: {stage_name}") + log = get_jenkins_log(jenkins_status["url"]) + lines.extend(["", "```", log, "```", ""]) + + return "\n".join(lines) + "\n" + + +def main(): + parser = argparse.ArgumentParser(description="Fetch CI results from GitHub Actions and Jenkins") + parser.add_argument("--wait", action="store_true", help="Wait for CI to complete") + parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT, help="Timeout in seconds (default: 1800)") + parser.add_argument("-o", "--output", default="ci_results.md", help="Output file (default: ci_results.md)") + parser.add_argument("--branch", help="Branch to check (default: current branch)") + parser.add_argument("--commit", help="Commit SHA to check (default: HEAD)") + args = parser.parse_args() + + branch, commit = get_git_info() + branch = args.branch or branch + commit = args.commit or commit + print(f"Fetching CI results for {branch} @ {commit[:7]}") + + start_time = time.monotonic() + while True: + gh_status, gh_run_id = get_github_actions_status(commit) + jenkins_status = get_jenkins_status(branch, commit) if branch != "HEAD" else None + + if not args.wait or is_complete(gh_status, jenkins_status): + break + + elapsed = time.monotonic() - start_time + if elapsed >= args.timeout: + print(f"Timeout after {int(elapsed)}s") + break + + print(f"CI still running, waiting {POLL_INTERVAL}s... ({int(elapsed)}s elapsed)") + time.sleep(POLL_INTERVAL) + + content = format_markdown(gh_status, gh_run_id, jenkins_status, commit, branch) + with open(args.output, "w") as f: + f.write(content) + print(f"Results written to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/selfdrive/assets/fonts/process.py b/selfdrive/assets/fonts/process.py index ddc8b3a868..30ccf9ca55 100755 --- a/selfdrive/assets/fonts/process.py +++ b/selfdrive/assets/fonts/process.py @@ -11,7 +11,7 @@ LANGUAGES_FILE = TRANSLATIONS_DIR / "languages.json" GLYPH_PADDING = 6 EXTRA_CHARS = "–‑✓×°§•X⚙✕◀▶✔⌫⇧␣○●↳çêüñ–‑✓×°§•€£¥" -UNIFONT_LANGUAGES = {"ar", "th", "zh-CHT", "zh-CHS", "ko", "ja"} +UNIFONT_LANGUAGES = {"th", "zh-CHT", "zh-CHS", "ko", "ja"} def _languages(): diff --git a/selfdrive/assets/icons_mici/adb_short.png b/selfdrive/assets/icons_mici/adb_short.png new file mode 100644 index 0000000000..c49226c858 --- /dev/null +++ b/selfdrive/assets/icons_mici/adb_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:263598da73c577c01cebd31ae78f45969ef8b335be1a5f55d54a696bb2982c0a +size 2062 diff --git a/selfdrive/assets/icons_mici/buttons/button_side_back.png b/selfdrive/assets/icons_mici/buttons/button_side_back.png deleted file mode 100644 index 3d648d34f1..0000000000 --- a/selfdrive/assets/icons_mici/buttons/button_side_back.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9df44871e9f5fa910622b0b92205b92a54d137dbdc3827b92e8622d85ff2e08e -size 5189 diff --git a/selfdrive/assets/icons_mici/buttons/button_side_back_pressed.png b/selfdrive/assets/icons_mici/buttons/button_side_back_pressed.png deleted file mode 100644 index e431cb0c73..0000000000 --- a/selfdrive/assets/icons_mici/buttons/button_side_back_pressed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:013b368b38b17d9b2ef6aaf0f498f672deed95888084b7287f42bdfba617cbb6 -size 10142 diff --git a/selfdrive/assets/icons_mici/buttons/button_side_check.png b/selfdrive/assets/icons_mici/buttons/button_side_check.png deleted file mode 100644 index 820b236066..0000000000 --- a/selfdrive/assets/icons_mici/buttons/button_side_check.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8fd563eec78d5ce4a8204c2f596789e1090cb3e26a35b4ffeacee4ab61968538 -size 8303 diff --git a/selfdrive/assets/icons_mici/buttons/button_side_check_pressed.png b/selfdrive/assets/icons_mici/buttons/button_side_check_pressed.png deleted file mode 100644 index 6c38508af9..0000000000 --- a/selfdrive/assets/icons_mici/buttons/button_side_check_pressed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0be8d5eddcd9f87acbf1daccf446be6218522120f64aee1ee0a3c0b31560f076 -size 15761 diff --git a/selfdrive/assets/icons_mici/buttons/toggle_dot_disabled.png b/selfdrive/assets/icons_mici/buttons/toggle_dot_disabled.png index 0e21bc1b5a..1ff4db45a5 100644 --- a/selfdrive/assets/icons_mici/buttons/toggle_dot_disabled.png +++ b/selfdrive/assets/icons_mici/buttons/toggle_dot_disabled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:613af9ed79bb26c60fbd19c094214f0881736c0e293f6d000b530cde0478a273 -size 2470 +oid sha256:89ac033d879beeb0a7fa1919838e0ec64b1a625a4aafc14f7b990c607a79b676 +size 2220 diff --git a/selfdrive/assets/icons_mici/exclamation_point.png b/selfdrive/assets/icons_mici/exclamation_point.png index 246fc015ec..ede3b638bc 100644 --- a/selfdrive/assets/icons_mici/exclamation_point.png +++ b/selfdrive/assets/icons_mici/exclamation_point.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b77579c099c688d1a27f356197fba9c2c8efcf4d391af580b4b29f0e70587919 -size 2086 +oid sha256:254b7f753b70c964847b686f0f71af751f2f49beea6ede4aeb333fe06062a257 +size 2289 diff --git a/selfdrive/assets/icons_mici/experimental_mode.png b/selfdrive/assets/icons_mici/experimental_mode.png index e0138bfd65..75850d08f5 100644 --- a/selfdrive/assets/icons_mici/experimental_mode.png +++ b/selfdrive/assets/icons_mici/experimental_mode.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb42b8d6259238beb26f286dc28fb2dc8d91b00fec1f7a7655296b5769439a15 -size 15690 +oid sha256:01841b602632c66ab14a8e52b874a1623f09641dc2ef0620f4e2d00bb4a913f3 +size 16243 diff --git a/selfdrive/assets/icons_mici/microphone.png b/selfdrive/assets/icons_mici/microphone.png index 9718a6b135..9af8f2f455 100644 --- a/selfdrive/assets/icons_mici/microphone.png +++ b/selfdrive/assets/icons_mici/microphone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17b6fe530598cbad34bcf31d4f21f929b792aacedef51b3ffef1941c86017811 -size 7331 +oid sha256:744dbaa68ee74e300cd46439bad79449c860e1c5c027304b0f382bd5383fba77 +size 6817 diff --git a/selfdrive/assets/icons_mici/offroad_alerts/green_wheel.png b/selfdrive/assets/icons_mici/offroad_alerts/green_wheel.png index 6a8351f6ee..08181ca35f 100644 --- a/selfdrive/assets/icons_mici/offroad_alerts/green_wheel.png +++ b/selfdrive/assets/icons_mici/offroad_alerts/green_wheel.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05f3626e790622a4ad90e982c4aacb612d0785a752339352a3187addf763e2e9 -size 13288 +oid sha256:3b11ee84d48972a2499cb29f01594d77a1a39692f6424a315a3f83262bc16087 +size 13481 diff --git a/selfdrive/assets/icons_mici/offroad_alerts/orange_warning.png b/selfdrive/assets/icons_mici/offroad_alerts/orange_warning.png index 13af475c6d..52e6836d4b 100644 --- a/selfdrive/assets/icons_mici/offroad_alerts/orange_warning.png +++ b/selfdrive/assets/icons_mici/offroad_alerts/orange_warning.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a877882a8dccb884bd35918f9f9b427a724a59e90a638e54f6fd5d0680ad173c -size 12137 +oid sha256:d548405a65ba4d4590c55866612dc6aa0e78d9278fc864ef60fe3e463edf4a68 +size 12169 diff --git a/selfdrive/assets/icons_mici/offroad_alerts/red_warning.png b/selfdrive/assets/icons_mici/offroad_alerts/red_warning.png index 83c3595b29..df608d3518 100644 --- a/selfdrive/assets/icons_mici/offroad_alerts/red_warning.png +++ b/selfdrive/assets/icons_mici/offroad_alerts/red_warning.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba944b208abed9b8b9752adb8017bd29cd2e98c89fb07ee5d0a595185c7564a5 -size 11898 +oid sha256:b6fc63326d34fbe72f6daf104d101ce19e547dbfe134427c067c957a7179df74 +size 12124 diff --git a/selfdrive/assets/icons_mici/onroad/blind_spot_left.png b/selfdrive/assets/icons_mici/onroad/blind_spot_left.png index 5d3b1e5d7b..fdc189b858 100644 --- a/selfdrive/assets/icons_mici/onroad/blind_spot_left.png +++ b/selfdrive/assets/icons_mici/onroad/blind_spot_left.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a23743d21bc8160e013625210654a55634e4ed58e60057b70e08761bac1c3680 -size 40406 +oid sha256:77b20a8c478d982412d556afb3a035b80b4aa9fe7a86aea761af4a42147d9435 +size 45297 diff --git a/selfdrive/assets/icons_mici/onroad/blind_spot_right.png b/selfdrive/assets/icons_mici/onroad/blind_spot_right.png index 67216078d9..b6cd7834ef 100644 --- a/selfdrive/assets/icons_mici/onroad/blind_spot_right.png +++ b/selfdrive/assets/icons_mici/onroad/blind_spot_right.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:acbfa3e38f0b9f422f5c1335ce20013852df2892b813db176a51918adc83ad58 -size 40979 +oid sha256:584cea202afff6dd20d67ae1a9cd6d2b8cc07598bccb91a8d1bac0142567308e +size 45489 diff --git a/selfdrive/assets/icons_mici/onroad/bookmark.png b/selfdrive/assets/icons_mici/onroad/bookmark.png index 207182276e..305561f509 100644 --- a/selfdrive/assets/icons_mici/onroad/bookmark.png +++ b/selfdrive/assets/icons_mici/onroad/bookmark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0d00d743b01c49c2b739127e9916a229caf8c48346d6d168863b080ddcaa409 -size 11124 +oid sha256:fd91685bf656e828648acf035a4737acb2c4709e8514cf0aa0a10fa470a9bb60 +size 11580 diff --git a/selfdrive/assets/icons_mici/onroad/bookmark_fill.png b/selfdrive/assets/icons_mici/onroad/bookmark_fill.png new file mode 100644 index 0000000000..531d5db1cf --- /dev/null +++ b/selfdrive/assets/icons_mici/onroad/bookmark_fill.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3f57346a1cf9a66f9fd746f87bcebb23b7a403e9d6e4fd7701b126abcdd47ea +size 18476 diff --git a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png index 04ffc24356..4129b13d92 100644 --- a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png +++ b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7eb870d01e5bf6c421e204026a4ea08e177731f2d6b5b17c4ad43c90c1c3e78 -size 23549 +oid sha256:cb89d9f11cf44992f92142aa5ad84e1ac700a2601aff2abab373e2a822af149e +size 11678 diff --git a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png index 540b2029a0..5b917f3a4a 100644 --- a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png +++ b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7b3bb76ee2359076339285ea6bced5b680e5b919a1b7dee163f36cd819c9ea1 -size 1746 +oid sha256:e2772c6a9fe9c57099d347ad49f0cb7c906593f1fdf0e6dde96d104baf0200b0 +size 1365 diff --git a/selfdrive/assets/icons_mici/onroad/eye_fill.png b/selfdrive/assets/icons_mici/onroad/eye_fill.png index 8f0e8ebfb1..78758a9809 100644 --- a/selfdrive/assets/icons_mici/onroad/eye_fill.png +++ b/selfdrive/assets/icons_mici/onroad/eye_fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51af75afbaf30abeaae1c99c7ad3e25cf5d5c90a2d6c799aad353b3302384b0a -size 4829 +oid sha256:07310879d093108435c0011846ae1184966db86443bc6e7ca036a6fa6123700b +size 4983 diff --git a/selfdrive/assets/icons_mici/onroad/eye_orange.png b/selfdrive/assets/icons_mici/onroad/eye_orange.png index b61b9b063c..932c71260b 100644 --- a/selfdrive/assets/icons_mici/onroad/eye_orange.png +++ b/selfdrive/assets/icons_mici/onroad/eye_orange.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88b2ecf3a9834d2b156bb632ec2090d7dc112e8ab61711ba645c03489d1c457f -size 29157 +oid sha256:7be447e56d649e0362ef650494b484e140a01ead31799ce43b266f5781c918d2 +size 36473 diff --git a/selfdrive/assets/icons_mici/onroad/glasses.png b/selfdrive/assets/icons_mici/onroad/glasses.png index 1ac4442f49..006972fd39 100644 --- a/selfdrive/assets/icons_mici/onroad/glasses.png +++ b/selfdrive/assets/icons_mici/onroad/glasses.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28c95c8970648d40b35b94724936a9ab7a6f4cbca367a40f01b86f9abedc70e5 -size 1587 +oid sha256:56de402482b5987ed9a0ff3f793a1c89f857304b34fbb8a3deb5b5d4a332be1c +size 3688 diff --git a/selfdrive/assets/icons_mici/onroad/onroad_fade.png b/selfdrive/assets/icons_mici/onroad/onroad_fade.png index bc12e57e17..3f823061b9 100644 --- a/selfdrive/assets/icons_mici/onroad/onroad_fade.png +++ b/selfdrive/assets/icons_mici/onroad/onroad_fade.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2a2cb4db429467783d7f721ffbed7838551e4aabf32771e73759c87b4a67bca -size 28880 +oid sha256:2aa6d04ba038f15a92868de6e6c7b04f624b4fe89d03bc3e9c4cd44cb729b24e +size 38317 diff --git a/selfdrive/assets/icons_mici/onroad/turn_signal_left.png b/selfdrive/assets/icons_mici/onroad/turn_signal_left.png index 48f52ff9ce..97b5cf1443 100644 --- a/selfdrive/assets/icons_mici/onroad/turn_signal_left.png +++ b/selfdrive/assets/icons_mici/onroad/turn_signal_left.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e845a211cf5d03f781efdd6eec4f8106e8dd85799ea59b51834a9099b479141 -size 30348 +oid sha256:f9f7d0554c0c79ab605c1119ffdef0a4f55196e53b75a65b6ac5218911e24a02 +size 45701 diff --git a/selfdrive/assets/icons_mici/onroad/turn_signal_right.png b/selfdrive/assets/icons_mici/onroad/turn_signal_right.png index 87ca979fbe..6bcb68dac5 100644 --- a/selfdrive/assets/icons_mici/onroad/turn_signal_right.png +++ b/selfdrive/assets/icons_mici/onroad/turn_signal_right.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:009005539f14acc29a4f5510b4e9531d2ba3667133644f6e0069c12b08ba0fd9 -size 35370 +oid sha256:7fae4872ab3c24d5e4c2be6150127a844f89bbdcadfccdff2dfed180e125d577 +size 45699 diff --git a/selfdrive/assets/icons_mici/settings.png b/selfdrive/assets/icons_mici/settings.png index e668ed1fe4..4ba7df9fdf 100644 --- a/selfdrive/assets/icons_mici/settings.png +++ b/selfdrive/assets/icons_mici/settings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38a52171bdc6feb3ddfd2d9f9e59db3dabd09fa0aafbc9f81137c59bd03b7c26 -size 2321 +oid sha256:14b457d2dc19d8658f525cc6989c9cfcf0edaf695b18767514242acbdbe2a6dd +size 2198 diff --git a/selfdrive/assets/icons_mici/settings/comma_icon.png b/selfdrive/assets/icons_mici/settings/comma_icon.png index 72a7c8c8f9..dd38a8938f 100644 --- a/selfdrive/assets/icons_mici/settings/comma_icon.png +++ b/selfdrive/assets/icons_mici/settings/comma_icon.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10f469a6f5d25d9e2b0b1aae51b4fbd06d2c7b8417613bb321c2a30bb7298dab -size 1392 +oid sha256:7ad4ee47ec6470f788a026f95ed86bf344f64f9cf3186c9c78927233d2694a1d +size 1388 diff --git a/selfdrive/assets/icons_mici/settings/developer/ssh.png b/selfdrive/assets/icons_mici/settings/developer/ssh.png index cd86937aea..0f17d04eca 100644 --- a/selfdrive/assets/icons_mici/settings/developer/ssh.png +++ b/selfdrive/assets/icons_mici/settings/developer/ssh.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c655994336b7da4ca986c6f27494bcab66e77f016ec9db8df271de53ed93e517 -size 1328 +oid sha256:b26133bee089627202d5e89a4e939ad23aaceb5d8e26d7381b1aea3ef892f2ee +size 2620 diff --git a/selfdrive/assets/icons_mici/settings/developer_icon.png b/selfdrive/assets/icons_mici/settings/developer_icon.png index af16c02912..f9d553c7c3 100644 --- a/selfdrive/assets/icons_mici/settings/developer_icon.png +++ b/selfdrive/assets/icons_mici/settings/developer_icon.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1f058c5640bd763d2f6927432a1daff1587770ea0d06f2e351a28462e9d8335 -size 1743 +oid sha256:ebb4f7ad9fd2f9fb3c69a38fbc00cbe690809b0ff202ffd4768ae5b699acc035 +size 1759 diff --git a/selfdrive/assets/icons_mici/settings/device/cameras.png b/selfdrive/assets/icons_mici/settings/device/cameras.png index c44c511275..ae9a88c4dc 100644 --- a/selfdrive/assets/icons_mici/settings/device/cameras.png +++ b/selfdrive/assets/icons_mici/settings/device/cameras.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77a1281979f0b50f0e109ead56a88a33b81ef5901dd1a4537eb3fa048e0d90de -size 1345 +oid sha256:5f47e636025e044977f278a35546e0fc971f48fd53c2eeafd3508e95c35f378f +size 3117 diff --git a/selfdrive/assets/icons_mici/settings/device/info.png b/selfdrive/assets/icons_mici/settings/device/info.png index cb16320693..9a29c46d0d 100644 --- a/selfdrive/assets/icons_mici/settings/device/info.png +++ b/selfdrive/assets/icons_mici/settings/device/info.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2649d36259700d32a0edef878647e76492b1bec2fe34ac8ea806d4e7e4c57855 -size 2668 +oid sha256:66858a5d3302333485fa391f7a9bb3a9b1ab4ae881e7fb47b04c3a4507011c94 +size 2613 diff --git a/selfdrive/assets/icons_mici/settings/device/language.png b/selfdrive/assets/icons_mici/settings/device/language.png index f6d57b3134..d2ef27de36 100644 --- a/selfdrive/assets/icons_mici/settings/device/language.png +++ b/selfdrive/assets/icons_mici/settings/device/language.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b982ac1b78b45487490d1dbbffed1f68735f6a35def502e882f706c30683aff -size 3664 +oid sha256:f646263b26de46f79cac836ef6865b0f25ddc91e386b99311723b68bd06693c9 +size 3304 diff --git a/selfdrive/assets/icons_mici/settings/device/lkas.png b/selfdrive/assets/icons_mici/settings/device/lkas.png index 186ea78fb9..80d37d4d5c 100644 --- a/selfdrive/assets/icons_mici/settings/device/lkas.png +++ b/selfdrive/assets/icons_mici/settings/device/lkas.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab6aeb6cba94acf948a0ad64a485db00bf1f3de1360ae4c57212f3f083b2bd24 -size 2554 +oid sha256:a05a41e66c7a24d461a4bbcdab0979031e5900e1db270af52ca363f0bed521f5 +size 2028 diff --git a/selfdrive/assets/icons_mici/settings/device/pair.png b/selfdrive/assets/icons_mici/settings/device/pair.png index f072b2363f..807d44335d 100644 --- a/selfdrive/assets/icons_mici/settings/device/pair.png +++ b/selfdrive/assets/icons_mici/settings/device/pair.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed671f4ad1523f0e66498af39e6075a0c19842ae05eddd00871a6e48ed3685d7 -size 1594 +oid sha256:678483230831d0a7d3dcad5f067a7b641e5d2ae0db477665dfc6c53a675eba18 +size 1779 diff --git a/selfdrive/assets/icons_mici/settings/device/power.png b/selfdrive/assets/icons_mici/settings/device/power.png index a2de14a4e8..711f1a4ab9 100644 --- a/selfdrive/assets/icons_mici/settings/device/power.png +++ b/selfdrive/assets/icons_mici/settings/device/power.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b45645ad9ff27776fdb1caa27827c526cae57f8bd4e23bd1160cb0094121ff2 -size 2338 +oid sha256:a34885e79f42d19b7777dd07e7ab51df344880cb770c48e0baaddb177c2ae938 +size 2228 diff --git a/selfdrive/assets/icons_mici/settings/device/reboot.png b/selfdrive/assets/icons_mici/settings/device/reboot.png index 6c89cd9fc2..298a85c504 100644 --- a/selfdrive/assets/icons_mici/settings/device/reboot.png +++ b/selfdrive/assets/icons_mici/settings/device/reboot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f24039f82d7399d02a155022de65b6dc3b8edcf17059a73a9fd3a9209e3f5575 -size 2360 +oid sha256:1356fe3ddda14568e9be1dca4e16ca9048852e3a27a3f531cd58d7d368485a82 +size 2362 diff --git a/selfdrive/assets/icons_mici/settings/device/uninstall.png b/selfdrive/assets/icons_mici/settings/device/uninstall.png index f9173711eb..53f8bc0e7d 100644 --- a/selfdrive/assets/icons_mici/settings/device/uninstall.png +++ b/selfdrive/assets/icons_mici/settings/device/uninstall.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:558ea538fb258079f9eb05fe048b2806c7635b9f0452af874b00cb8d79b45f9b -size 2421 +oid sha256:50a8ce4fa8ff7f5b0f56ba0dc65b4802dc0be2dc0967b5cb3a15e3b79a4e513e +size 2424 diff --git a/selfdrive/assets/icons_mici/settings/device/up_to_date.png b/selfdrive/assets/icons_mici/settings/device/up_to_date.png index ee925458d3..e09f7d3308 100644 --- a/selfdrive/assets/icons_mici/settings/device/up_to_date.png +++ b/selfdrive/assets/icons_mici/settings/device/up_to_date.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4510e65775c6001758ebcf4dc13e9fa561cce5159d1fd54fbb506f22d3c7bdf3 -size 3149 +oid sha256:61bc44b6e0f99640434d6abcb64880c7bf575eda5cdcf7d74cba7d73307dd39a +size 2739 diff --git a/selfdrive/assets/icons_mici/settings/device/update.png b/selfdrive/assets/icons_mici/settings/device/update.png index cc05931b03..498c066191 100644 --- a/selfdrive/assets/icons_mici/settings/device/update.png +++ b/selfdrive/assets/icons_mici/settings/device/update.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6137349218ea22adba44f46a096afe2efc35536b2251192ed0ea61be443a3c5 -size 2493 +oid sha256:f28cdeaba9146521335bc11ad60a8e0368eb0ed1381e88b35a12a6138ba22ed6 +size 2409 diff --git a/selfdrive/assets/icons_mici/settings/device_icon.png b/selfdrive/assets/icons_mici/settings/device_icon.png index 0caf0d07ce..6a716e4dfd 100644 --- a/selfdrive/assets/icons_mici/settings/device_icon.png +++ b/selfdrive/assets/icons_mici/settings/device_icon.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db20bea98259b204be634ce0d9a23fbfdcfc73a324fc0aac0f9ac54e1c51556d -size 2443 +oid sha256:2273629450aa870f0964dd285721c35d3d313fb8b4684122215a65844ae744d0 +size 1888 diff --git a/selfdrive/assets/icons_mici/settings/firehose.png b/selfdrive/assets/icons_mici/settings/firehose.png new file mode 100644 index 0000000000..37451c0482 --- /dev/null +++ b/selfdrive/assets/icons_mici/settings/firehose.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:416656861380981acc114e5285b448d6e4dc42b98539d0ba16821cbc3db89208 +size 1364 diff --git a/selfdrive/assets/icons_mici/settings/horizontal_scroll_indicator.png b/selfdrive/assets/icons_mici/settings/horizontal_scroll_indicator.png new file mode 100644 index 0000000000..39dd7b1947 --- /dev/null +++ b/selfdrive/assets/icons_mici/settings/horizontal_scroll_indicator.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af8d5ecb6468442361462aa838a2d234b1256b8139418be8ef2962e4350cfbef +size 2176 diff --git a/selfdrive/assets/icons_mici/settings/keyboard/backspace.png b/selfdrive/assets/icons_mici/settings/keyboard/backspace.png index 342f8e28da..53ff00c2ae 100644 --- a/selfdrive/assets/icons_mici/settings/keyboard/backspace.png +++ b/selfdrive/assets/icons_mici/settings/keyboard/backspace.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:116bbbd1509e6644f7b65b8dacd2402b0918785bd80207504a99ab7e13ab738f -size 2049 +oid sha256:69bb4a401429c3fdf473778f751288b2aafea27eb13f09b20e83d55212f084ba +size 1963 diff --git a/selfdrive/assets/icons_mici/settings/keyboard/caps_lock.png b/selfdrive/assets/icons_mici/settings/keyboard/caps_lock.png index d63cc56fbc..2d173bfc9f 100644 --- a/selfdrive/assets/icons_mici/settings/keyboard/caps_lock.png +++ b/selfdrive/assets/icons_mici/settings/keyboard/caps_lock.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e8c7fec57640de6bfa8d0ede977e40920a8e651b68ed14e3d6c1850e702f3e3 -size 1399 +oid sha256:563c211fd98018e24418235602e596f3a481f04fddde0a14590e563474fcffd2 +size 1423 diff --git a/selfdrive/assets/icons_mici/settings/keyboard/caps_lower.png b/selfdrive/assets/icons_mici/settings/keyboard/caps_lower.png index eb38934302..a3ce71f049 100644 --- a/selfdrive/assets/icons_mici/settings/keyboard/caps_lower.png +++ b/selfdrive/assets/icons_mici/settings/keyboard/caps_lower.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7dab3af28938e9c3ad7b6c3b60526bb76498b0103c7276d90c4bff3622f07d0 -size 1157 +oid sha256:6f81811ea9cdc409d5549035ca928c76e22396193e1cefb6cacab3747ee0c297 +size 1142 diff --git a/selfdrive/assets/icons_mici/settings/keyboard/caps_upper.png b/selfdrive/assets/icons_mici/settings/keyboard/caps_upper.png index 4a2cae6c8a..7c147bc07b 100644 --- a/selfdrive/assets/icons_mici/settings/keyboard/caps_upper.png +++ b/selfdrive/assets/icons_mici/settings/keyboard/caps_upper.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c5a88a0e8e810115b6d497d3e230d866bd96a715ddac632f48c78b40e1df702 -size 1059 +oid sha256:60875e73dd9659122c9248d8e99d5cfd301d68dabeec2cb42cebce812c9baae9 +size 1102 diff --git a/selfdrive/assets/icons_mici/settings/keyboard/confirm.png b/selfdrive/assets/icons_mici/settings/keyboard/confirm.png deleted file mode 100644 index 09b180e97f..0000000000 --- a/selfdrive/assets/icons_mici/settings/keyboard/confirm.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:32ce109a9fe4814bb9bed88f67d85292791f4a6d7c162e07561920221ac38b2d -size 1411 diff --git a/selfdrive/assets/icons_mici/settings/keyboard/enter.png b/selfdrive/assets/icons_mici/settings/keyboard/enter.png new file mode 100644 index 0000000000..0b7fc95c51 --- /dev/null +++ b/selfdrive/assets/icons_mici/settings/keyboard/enter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3dd956d5ccfce01a01bea74ef59c9e73dfca406a5ff9ac62417203afa6027fba +size 5620 diff --git a/selfdrive/assets/icons_mici/settings/keyboard/enter_disabled.png b/selfdrive/assets/icons_mici/settings/keyboard/enter_disabled.png new file mode 100644 index 0000000000..251d5d8d14 --- /dev/null +++ b/selfdrive/assets/icons_mici/settings/keyboard/enter_disabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dd1c2308872729d58adab390030ae9c987dc7908f0c39391651ea2b6cb620c5 +size 2445 diff --git a/selfdrive/assets/icons_mici/settings/keyboard/space.png b/selfdrive/assets/icons_mici/settings/keyboard/space.png index 778d1847d7..3d61109721 100644 --- a/selfdrive/assets/icons_mici/settings/keyboard/space.png +++ b/selfdrive/assets/icons_mici/settings/keyboard/space.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b04d17f3b0340a94210efa5c9547e0ac340dd6b6dd9ac1f81ba5eb3f89f405d -size 619 +oid sha256:f431e428772991323ee3ce662479e1ab29c3d80a72b93cf9c9673716ba245d5f +size 654 diff --git a/selfdrive/assets/icons_mici/settings/manual_icon.png b/selfdrive/assets/icons_mici/settings/manual_icon.png deleted file mode 100644 index 100b29da45..0000000000 --- a/selfdrive/assets/icons_mici/settings/manual_icon.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:957330e9fbc8c03f05dbef8097178a40efc0fc52a6faf7a9917f97046d9a5e99 -size 1559 diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_full.png b/selfdrive/assets/icons_mici/settings/network/cell_strength_full.png index 4bf0cd8726..13f70386d4 100644 --- a/selfdrive/assets/icons_mici/settings/network/cell_strength_full.png +++ b/selfdrive/assets/icons_mici/settings/network/cell_strength_full.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a981d5c5558859b283cb6321c84eec947f82fc2dea8dbdd19b66781e4d3f61f -size 1060 +oid sha256:fb7af523411c5ed75c6e1418dfc2a379486f6dbd7f2f1c281d3ff54e1ea7810e +size 777 diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_high.png b/selfdrive/assets/icons_mici/settings/network/cell_strength_high.png index df6d009335..1fea6d23b8 100644 --- a/selfdrive/assets/icons_mici/settings/network/cell_strength_high.png +++ b/selfdrive/assets/icons_mici/settings/network/cell_strength_high.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58da16ede432cf89096c11dc0f4ea098735863fb09a1d655cb06de8a112bd263 -size 1205 +oid sha256:db86e176e016458fcff00d40e37636a808977e0cc01bcc9c04b31a1001562de8 +size 936 diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_low.png b/selfdrive/assets/icons_mici/settings/network/cell_strength_low.png index c3323a9fea..d763f86c7f 100644 --- a/selfdrive/assets/icons_mici/settings/network/cell_strength_low.png +++ b/selfdrive/assets/icons_mici/settings/network/cell_strength_low.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:031bbd50c34d8fd5e71bdc292ba3e50b28a13c56a48dc84117723f1b35b42f51 -size 1224 +oid sha256:1cd0b3a00db36ee7eacf5887d07d40e5351fb441d98643a02df4c742cd1e935d +size 945 diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_medium.png b/selfdrive/assets/icons_mici/settings/network/cell_strength_medium.png index 64ab947c53..148ee63e99 100644 --- a/selfdrive/assets/icons_mici/settings/network/cell_strength_medium.png +++ b/selfdrive/assets/icons_mici/settings/network/cell_strength_medium.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ccb5f2227c72dd28e40c9f19965abe007cbd7b47cdca924907dc9fad906f5c81 -size 1219 +oid sha256:25724acfe0c261070b103ef5933053d5dd8b726ece42d0e5f715f05c67be2294 +size 956 diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_none.png b/selfdrive/assets/icons_mici/settings/network/cell_strength_none.png index 6cdef706bd..c6d82ac316 100644 --- a/selfdrive/assets/icons_mici/settings/network/cell_strength_none.png +++ b/selfdrive/assets/icons_mici/settings/network/cell_strength_none.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92c195721fe2b4ca42176077bf4ca3484cdfc314e961f1431b2296476bcae891 -size 1178 +oid sha256:cb0aeb6260bcd0642204f842112479f4b19b350db9addae5e14c9c5131bcf956 +size 781 diff --git a/selfdrive/assets/icons_mici/settings/network/new/lock.png b/selfdrive/assets/icons_mici/settings/network/new/lock.png index 0a0b18c7a9..9fc152d3db 100644 --- a/selfdrive/assets/icons_mici/settings/network/new/lock.png +++ b/selfdrive/assets/icons_mici/settings/network/new/lock.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40dbbb3000e1137ec11fe658fbfebae7cadfc91356953317335f9bb70fcb40d3 -size 1235 +oid sha256:782161f35b4925c7063c441b0c341331c814614cf241f21b4e70134280c630f0 +size 1182 diff --git a/selfdrive/assets/icons_mici/settings/network/new/trash.png b/selfdrive/assets/icons_mici/settings/network/new/trash.png index 99e1a2e246..81e5f13e43 100644 --- a/selfdrive/assets/icons_mici/settings/network/new/trash.png +++ b/selfdrive/assets/icons_mici/settings/network/new/trash.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:efabf98ed66fe4447c0f13c74aec681b084de780c551ce18258c79636d4123c5 -size 1524 +oid sha256:9074162bf0469fc5ab0b5711a121289a983c887161df269ac120edd8fd024499 +size 1533 diff --git a/selfdrive/assets/icons_mici/settings/network/tethering.png b/selfdrive/assets/icons_mici/settings/network/tethering.png index 9e7b90be41..4bb416b0b1 100644 --- a/selfdrive/assets/icons_mici/settings/network/tethering.png +++ b/selfdrive/assets/icons_mici/settings/network/tethering.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2907ce46d1b6e676402f390c530955b65e76baf0b77fafc0616c50b988b3994c -size 1609 +oid sha256:b1e322ea6e57b05b3515fcd4e9100f890e6ff80607c11360b7927fa5a9765beb +size 2752 diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_full.png b/selfdrive/assets/icons_mici/settings/network/wifi_strength_full.png index 1a1655fddc..fe81ffa572 100644 --- a/selfdrive/assets/icons_mici/settings/network/wifi_strength_full.png +++ b/selfdrive/assets/icons_mici/settings/network/wifi_strength_full.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2715ea698eccb3648ab96cbddf897ea1842acbc1eb9667bc6f34aba82d0896b -size 1976 +oid sha256:73c76e5240bdff64c1d1ed0ac2bb9c3fadb2fd61fbf8dc710b812757af8bcf6c +size 2026 diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_low.png b/selfdrive/assets/icons_mici/settings/network/wifi_strength_low.png index 4d64d8062f..2649cc89dc 100644 --- a/selfdrive/assets/icons_mici/settings/network/wifi_strength_low.png +++ b/selfdrive/assets/icons_mici/settings/network/wifi_strength_low.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58d839402c6f002ba8d2217888190b338fc3ac13d372df0988fac7bf95b89302 -size 2111 +oid sha256:e66cc6174a54177793c42ef3525a9aa1592e05b0abb677442c7226269d1371a5 +size 2196 diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_medium.png b/selfdrive/assets/icons_mici/settings/network/wifi_strength_medium.png index 2d53a20cef..8881833375 100644 --- a/selfdrive/assets/icons_mici/settings/network/wifi_strength_medium.png +++ b/selfdrive/assets/icons_mici/settings/network/wifi_strength_medium.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9918724409dbfa1973a097a692c2f57e45cc2bc0ce71c498ef3e02aa82559d3 -size 2128 +oid sha256:7948a9234f2bc996aefb3a9e58a37c06ebbf54e8e4596e47800f78ef7e81961f +size 2231 diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_none.png b/selfdrive/assets/icons_mici/settings/network/wifi_strength_none.png index 482a0e1042..848d7849a2 100644 --- a/selfdrive/assets/icons_mici/settings/network/wifi_strength_none.png +++ b/selfdrive/assets/icons_mici/settings/network/wifi_strength_none.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fcef95eb18e2db566b907ae99b8d8f450424b3b7823fdc24cdfe066ccf64378 -size 2141 +oid sha256:a57ea402448dacc2026631174e448b6254698fe92309221576400cbf28196936 +size 2195 diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_slash.png b/selfdrive/assets/icons_mici/settings/network/wifi_strength_slash.png index 38ddff84b7..4457a3fcd2 100644 --- a/selfdrive/assets/icons_mici/settings/network/wifi_strength_slash.png +++ b/selfdrive/assets/icons_mici/settings/network/wifi_strength_slash.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73e4ae4741a039f41d79827c40be6da83f8c6eb79e9103db2dfec718ca96efb7 -size 2512 +oid sha256:7e6d166bdbbcdc106e7cd4a44ba85848888f18a6ef34e86daac8e12a3f519443 +size 2318 diff --git a/selfdrive/assets/icons_mici/settings/toggles_icon.png b/selfdrive/assets/icons_mici/settings/toggles_icon.png deleted file mode 100644 index ccb343e8ed..0000000000 --- a/selfdrive/assets/icons_mici/settings/toggles_icon.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0297535eb73bea71e87c363dc12385bb9163b81403797e50966b20259f725542 -size 2528 diff --git a/selfdrive/assets/icons_mici/settings/vertical_scroll_indicator.png b/selfdrive/assets/icons_mici/settings/vertical_scroll_indicator.png deleted file mode 100644 index 77d9a77d6f..0000000000 --- a/selfdrive/assets/icons_mici/settings/vertical_scroll_indicator.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:88e6c50358f627fc714c1e9883143aeed00baabeab16132e16001aa1051e5eb8 -size 1272 diff --git a/selfdrive/assets/icons_mici/setup/back_new.png b/selfdrive/assets/icons_mici/setup/back_new.png index c4834a5649..20e7fe3b88 100644 --- a/selfdrive/assets/icons_mici/setup/back_new.png +++ b/selfdrive/assets/icons_mici/setup/back_new.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7198352d23952d0f2fbc128f20523ea6f2f2b7e378aa495da748a0e34f192806 -size 1641 +oid sha256:d29a9c295b33b3164c37a68ad77795595e6ac877a5b308d28112b0315ecd498f +size 1687 diff --git a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png index 92993e3e00..dfb9799b0b 100644 --- a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png +++ b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b7dce550c008ff7a65ed19ccf308ecf92cd0118bb544978b7dd7393c5c27ae5 -size 809 +oid sha256:2290105f9b055b3c3d482d883d148de3418cad07b653133b0f61137e1976c407 +size 1412 diff --git a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png index 53a837afbe..fa29be1827 100644 --- a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png +++ b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e102b8b2e71a25d9f818b37d6f75ed958430cb765a07ae50713995779fb6a886 -size 1388 +oid sha256:ec9691d2572e2e084f0b3c99a1dcd0daadf5040d16c02347ffec9dd5466c061a +size 1438 diff --git a/selfdrive/assets/icons_mici/setup/green_car.png b/selfdrive/assets/icons_mici/setup/green_car.png deleted file mode 100644 index 867cadbbd6..0000000000 --- a/selfdrive/assets/icons_mici/setup/green_car.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ce8a34777e0b185f457b98845aa17fe6b5192ca46101463aecd21a9e04c0f0f0 -size 13281 diff --git a/selfdrive/assets/icons_mici/setup/green_dm.png b/selfdrive/assets/icons_mici/setup/green_dm.png index d41edd4c2a..87f4ffe788 100644 --- a/selfdrive/assets/icons_mici/setup/green_dm.png +++ b/selfdrive/assets/icons_mici/setup/green_dm.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78795eaa5e0be5fa369e172c02f5bd4b06d20f44363ccb8cbd02cb181b13e529 -size 14289 +oid sha256:8b6d7747dd6bbf47d9782fc0d847c224b933f6616218ade1f9220018aa9d6acc +size 15052 diff --git a/selfdrive/assets/icons_mici/setup/green_info.png b/selfdrive/assets/icons_mici/setup/green_info.png index 309e56e6ee..57e005abd6 100644 --- a/selfdrive/assets/icons_mici/setup/green_info.png +++ b/selfdrive/assets/icons_mici/setup/green_info.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b0b1777d5bed7149982af9f2abab3fab7b6c576e3d53cf2c459804c6ec9ca1e -size 3957 +oid sha256:5055bc385a1de674e6f3cbafdb611ee4b1088de2a3c357bce76f6a192226c952 +size 14154 diff --git a/selfdrive/assets/icons_mici/setup/green_pedal.png b/selfdrive/assets/icons_mici/setup/green_pedal.png deleted file mode 100644 index 2dd18f489a..0000000000 --- a/selfdrive/assets/icons_mici/setup/green_pedal.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6cadcda59bc861a1e710e0a8ac67024bdcc44b5f9261abbf098ff11cefb1da51 -size 12209 diff --git a/selfdrive/assets/icons_mici/setup/orange_dm.png b/selfdrive/assets/icons_mici/setup/orange_dm.png index 74cce9d975..97df767a98 100644 --- a/selfdrive/assets/icons_mici/setup/orange_dm.png +++ b/selfdrive/assets/icons_mici/setup/orange_dm.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38a108f96f85a154b698693b07f2e4214124b8f2545b7c4490cea0aa998d75fd -size 11855 +oid sha256:9c45ab0b949c1c71651f9f48cf6ff10196d64eb85e042b063e92b1d7ca02dcb5 +size 13155 diff --git a/selfdrive/assets/icons_mici/setup/red_warning.png b/selfdrive/assets/icons_mici/setup/red_warning.png index ed0634079b..387794cf13 100644 --- a/selfdrive/assets/icons_mici/setup/red_warning.png +++ b/selfdrive/assets/icons_mici/setup/red_warning.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:448d3e7214a77b02b32020ddb440ccd8fe72e110493a51cc10901c8242e72ca8 -size 3185 +oid sha256:e8e8bc3c15df7512a81b902e47fb069eff1370c833095d3b25f3866efb815fff +size 11123 diff --git a/selfdrive/assets/icons_mici/setup/restore.png b/selfdrive/assets/icons_mici/setup/restore.png index 6aa6c6b851..5eff924040 100644 --- a/selfdrive/assets/icons_mici/setup/restore.png +++ b/selfdrive/assets/icons_mici/setup/restore.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d6b99696163cac1867d46998af9e53e212b82641b33c93b51276671f400a5ac -size 2962 +oid sha256:1f5ee67cd334d259ac33f932281db36533877009b5769c92d9cff3054fd5627c +size 2942 diff --git a/selfdrive/assets/icons_mici/setup/scroll_down_indicator.png b/selfdrive/assets/icons_mici/setup/scroll_down_indicator.png index 4d74d86075..3cd26e5181 100644 --- a/selfdrive/assets/icons_mici/setup/scroll_down_indicator.png +++ b/selfdrive/assets/icons_mici/setup/scroll_down_indicator.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52535e34e27b0341f7690a72dc16555eeb6e032bc2c2cde0786469852fdf5987 -size 1267 +oid sha256:a733c425113a7f6ff5ec3dc50ef94b5481c0f2d306e33d1485be8ee6b2798532 +size 1136 diff --git a/selfdrive/assets/icons_mici/setup/small_slider/slider_arrow.png b/selfdrive/assets/icons_mici/setup/small_slider/slider_arrow.png index bbf1d96254..acf5b17414 100644 --- a/selfdrive/assets/icons_mici/setup/small_slider/slider_arrow.png +++ b/selfdrive/assets/icons_mici/setup/small_slider/slider_arrow.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8425c56cb413ba757c94febe0332ce472dbf1472236b03cc4e627746fb86d701 -size 1149 +oid sha256:75a6557935075a646b17d083202832daafb263d4cfa38aea2af407afc04e2ef4 +size 1312 diff --git a/selfdrive/assets/icons_mici/setup/warning.png b/selfdrive/assets/icons_mici/setup/warning.png index 806eea28b7..1b7839f47f 100644 --- a/selfdrive/assets/icons_mici/setup/warning.png +++ b/selfdrive/assets/icons_mici/setup/warning.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bc7a85a0672183d80817f337084060465e143362037955025c11bc8ac531076 -size 3247 +oid sha256:7584d32ac0231381e38646fdac2f71b4517905ef22024f01bd9e124d3918f33a +size 9194 diff --git a/selfdrive/assets/icons_mici/ssh_short.png b/selfdrive/assets/icons_mici/ssh_short.png new file mode 100644 index 0000000000..699ddd72e8 --- /dev/null +++ b/selfdrive/assets/icons_mici/ssh_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef1735e6effcb625ea618fa35a6b908b28ca483d5997e15241d48e2d3d29819e +size 1433 diff --git a/selfdrive/assets/icons_mici/turn_intent_left.png b/selfdrive/assets/icons_mici/turn_intent_left.png index 6c2c47e882..3934200c9d 100644 --- a/selfdrive/assets/icons_mici/turn_intent_left.png +++ b/selfdrive/assets/icons_mici/turn_intent_left.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ead8287b7041c32456e13721c238a71933256ca3d2b7e649c8f8731585eb5de8 -size 906 +oid sha256:001cb8227eaaff5367055395d9b3ccd5822f9a47276091832d8ad28b074d77c9 +size 914 diff --git a/selfdrive/assets/icons_mici/turn_intent_right.png b/selfdrive/assets/icons_mici/turn_intent_right.png index 03a7245e76..e342778731 100644 --- a/selfdrive/assets/icons_mici/turn_intent_right.png +++ b/selfdrive/assets/icons_mici/turn_intent_right.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fe0532f7040aae78baa85c4cca44f5c939adb6a6f15889e2ca036f4a493f848 -size 935 +oid sha256:7b7e0194a8b9009e493cdce35cd15711596a54227c740e9d6419a3891c6c4037 +size 912 diff --git a/selfdrive/assets/icons_mici/wheel.png b/selfdrive/assets/icons_mici/wheel.png index f122349b82..a43bcb3b99 100644 --- a/selfdrive/assets/icons_mici/wheel.png +++ b/selfdrive/assets/icons_mici/wheel.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc3ef0c8c3038d75f99df2c565a361107bc903944d1afe91de0cbed9f6ca062a -size 2725 +oid sha256:8cf9c6361ed82551eb99e028e0a75ff56b72ca856ccf7c9a76afe6745434980a +size 2720 diff --git a/selfdrive/assets/icons_mici/wheel_critical.png b/selfdrive/assets/icons_mici/wheel_critical.png index c0e5e8619e..676b0b4d71 100644 --- a/selfdrive/assets/icons_mici/wheel_critical.png +++ b/selfdrive/assets/icons_mici/wheel_critical.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12783dc05ea6dae2647ac3a3a7c8391d520c3f0cf2f458333a357ee9633eb6c4 -size 10909 +oid sha256:4c3d9082b295f9e5ddef93f8d4e9cb961ea2374c7affd26394bbccb26e7137b2 +size 11023 diff --git a/selfdrive/car/tests/big_cars_test.sh b/selfdrive/car/tests/big_cars_test.sh index 863b8bead0..bb6e82dd0e 100755 --- a/selfdrive/car/tests/big_cars_test.sh +++ b/selfdrive/car/tests/big_cars_test.sh @@ -6,7 +6,6 @@ cd $BASEDIR export MAX_EXAMPLES=300 export INTERNAL_SEG_CNT=300 -export FILEREADER_CACHE=1 export INTERNAL_SEG_LIST=selfdrive/car/tests/test_models_segs.txt cd selfdrive/car/tests && pytest test_models.py test_car_interfaces.py diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 47d105f993..d0eb40d096 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -22,15 +22,17 @@ from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext import La # Additionally, there is friction in the steering wheel that needs # to be overcome to move it at all, this is compensated for too. -KP = 1.0 -KI = 0.3 -KD = 0.0 +KP = 0.8 +KI = 0.15 + INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30] KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP] LP_FILTER_CUTOFF_HZ = 1.2 +JERK_LOOKAHEAD_SECONDS = 0.19 +JERK_GAIN = 0.3 LAT_ACCEL_REQUEST_BUFFER_SECONDS = 1.0 -VERSION = 0 +VERSION = 1 class LatControlTorque(LatControl): def __init__(self, CP, CP_SP, CI, dt): @@ -38,13 +40,13 @@ class LatControlTorque(LatControl): self.torque_params = CP.lateralTuning.torque.as_builder() self.torque_from_lateral_accel = CI.torque_from_lateral_accel() self.lateral_accel_from_torque = CI.lateral_accel_from_torque() - self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, KD, rate=1/self.dt) + self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, rate=1/self.dt) self.update_limits() self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg self.lat_accel_request_buffer_len = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / self.dt) self.lat_accel_request_buffer = deque([0.] * self.lat_accel_request_buffer_len , maxlen=self.lat_accel_request_buffer_len) - self.previous_measurement = 0.0 - self.measurement_rate_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt) + self.lookahead_frames = int(JERK_LOOKAHEAD_SECONDS / self.dt) + self.jerk_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt) self.extension = LatControlTorqueExt(self, CP, CP_SP, CI) @@ -65,44 +67,38 @@ class LatControlTorque(LatControl): pid_log = log.ControlsState.LateralTorqueState.new_message() pid_log.version = VERSION + measured_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) + measurement = measured_curvature * CS.vEgo ** 2 + future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2 + self.lat_accel_request_buffer.append(future_desired_lateral_accel) + + roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY + curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) + lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2 + + delay_frames = int(np.clip(lat_delay / self.dt + 1, 1, self.lat_accel_request_buffer_len)) + expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames] + setpoint = expected_lateral_accel + error = setpoint - measurement + + lookahead_idx = int(np.clip(-delay_frames + self.lookahead_frames, -self.lat_accel_request_buffer_len+1, -2)) + raw_lateral_jerk = (self.lat_accel_request_buffer[lookahead_idx+1] - self.lat_accel_request_buffer[lookahead_idx-1]) / (2 * self.dt) + desired_lateral_jerk = self.jerk_filter.update(raw_lateral_jerk) + gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation + ff = gravity_adjusted_future_lateral_accel + # latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll + ff -= self.torque_params.latAccelOffset + ff += get_friction(error + JERK_GAIN * desired_lateral_jerk, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) + if not active: output_torque = 0.0 pid_log.active = False else: - measured_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) - roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY - curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) - lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2 - - delay_frames = int(np.clip(lat_delay / self.dt, 1, self.lat_accel_request_buffer_len)) - expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames] - # TODO factor out lateral jerk from error to later replace it with delay independent alternative - future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2 - self.lat_accel_request_buffer.append(future_desired_lateral_accel) - gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation - desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / lat_delay - - measurement = measured_curvature * CS.vEgo ** 2 - measurement_rate = self.measurement_rate_filter.update((measurement - self.previous_measurement) / self.dt) - self.previous_measurement = measurement - - setpoint = lat_delay * desired_lateral_jerk + expected_lateral_accel - error = setpoint - measurement - # do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly pid_log.error = float(error) - ff = gravity_adjusted_future_lateral_accel - # latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll - ff -= self.torque_params.latAccelOffset - # TODO jerk is weighted by lat_delay for legacy reasons, but should be made independent of it - ff += get_friction(error, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 - output_lataccel = self.pid.update(pid_log.error, - -measurement_rate, - feedforward=ff, - speed=CS.vEgo, - freeze_integrator=freeze_integrator) + output_lataccel = self.pid.update(pid_log.error, speed=CS.vEgo, feedforward=ff, freeze_integrator=freeze_integrator) output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params) # Lateral acceleration torque controller extension updates diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript b/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript index 164b965142..7a6c02a538 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', 'pandad_python', 'np_version') +Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'np_version') gen = "c_generated_code" @@ -67,7 +67,7 @@ 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, pandad_python]) +lenv.Depends(generated_long, [msgq_python, common_python]) lenv["CFLAGS"].append("-DACADOS_WITH_QPOASES") lenv["CXXFLAGS"].append("-DACADOS_WITH_QPOASES") diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py index 3f9d8245bd..efdef9dd71 100755 --- a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py +++ b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @@ -22,7 +22,8 @@ LONG_MPC_DIR = os.path.dirname(os.path.abspath(__file__)) EXPORT_DIR = os.path.join(LONG_MPC_DIR, "c_generated_code") JSON_FILE = os.path.join(LONG_MPC_DIR, "acados_ocp_long.json") -SOURCES = ['lead0', 'lead1', 'cruise', 'e2e'] +LongitudinalPlanSource = log.LongitudinalPlan.LongitudinalPlanSource +MPC_SOURCES = (LongitudinalPlanSource.lead0, LongitudinalPlanSource.lead1, LongitudinalPlanSource.cruise) X_DIM = 3 U_DIM = 1 @@ -35,7 +36,7 @@ X_EGO_OBSTACLE_COST = 3. X_EGO_COST = 0. V_EGO_COST = 0. A_EGO_COST = 0. -J_EGO_COST = 5.0 +J_EGO_COST = 5. A_CHANGE_COST = 200. DANGER_ZONE_COST = 100. CRASH_DISTANCE = .25 @@ -43,7 +44,6 @@ LEAD_DANGER_FACTOR = 0.75 LIMIT_COST = 1e6 ACADOS_SOLVER_TYPE = 'SQP_RTI' - # Fewer timestamps don't hurt performance and lead to # much better convergence of the MPC with low iterations N = 12 @@ -57,6 +57,7 @@ COMFORT_BRAKE = 2.5 STOP_DISTANCE = 6.0 CRUISE_MIN_ACCEL = -1.2 CRUISE_MAX_ACCEL = 1.6 +MIN_X_LEAD_FACTOR = 0.5 def get_jerk_factor(personality=log.LongitudinalPersonality.standard): if personality==log.LongitudinalPersonality.relaxed: @@ -85,20 +86,12 @@ def get_stopped_equivalence_factor(v_lead): def get_safe_obstacle_distance(v_ego, t_follow): return (v_ego**2) / (2 * COMFORT_BRAKE) + t_follow * v_ego + STOP_DISTANCE -def desired_follow_distance(v_ego, v_lead, t_follow=None): - if t_follow is None: - t_follow = get_T_FOLLOW() - return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead) - - def gen_long_model(): model = AcadosModel() model.name = MODEL_NAME - # set up states & controls - x_ego = SX.sym('x_ego') - v_ego = SX.sym('v_ego') - a_ego = SX.sym('a_ego') + # states + x_ego, v_ego, a_ego = SX.sym('x_ego'), SX.sym('v_ego'), SX.sym('a_ego') model.x = vertcat(x_ego, v_ego, a_ego) # controls @@ -115,10 +108,10 @@ def gen_long_model(): a_min = SX.sym('a_min') a_max = SX.sym('a_max') x_obstacle = SX.sym('x_obstacle') - prev_a = SX.sym('prev_a') + a_prev = SX.sym('a_prev') lead_t_follow = SX.sym('lead_t_follow') lead_danger_factor = SX.sym('lead_danger_factor') - model.p = vertcat(a_min, a_max, x_obstacle, prev_a, lead_t_follow, lead_danger_factor) + model.p = vertcat(a_min, a_max, x_obstacle, a_prev, lead_t_follow, lead_danger_factor) # dynamics model f_expl = vertcat(v_ego, a_ego, j_ego) @@ -126,7 +119,6 @@ def gen_long_model(): model.f_expl_expr = f_expl return model - def gen_long_ocp(): ocp = AcadosOcp() ocp.model = gen_long_model() @@ -151,7 +143,7 @@ def gen_long_ocp(): a_min, a_max = ocp.model.p[0], ocp.model.p[1] x_obstacle = ocp.model.p[2] - prev_a = ocp.model.p[3] + a_prev = ocp.model.p[3] lead_t_follow = ocp.model.p[4] lead_danger_factor = ocp.model.p[5] @@ -168,7 +160,7 @@ def gen_long_ocp(): x_ego, v_ego, a_ego, - a_ego - prev_a, + a_ego - a_prev, j_ego] ocp.model.cost_y_expr = vertcat(*costs) ocp.model.cost_y_expr_e = vertcat(*costs[:-1]) @@ -222,30 +214,31 @@ def gen_long_ocp(): class LongitudinalMpc: - def __init__(self, mode='acc', dt=DT_MDL): - self.mode = mode + def __init__(self, dt=DT_MDL): self.dt = dt self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N) self.reset() - self.source = SOURCES[2] + self.source = LongitudinalPlanSource.cruise def reset(self): - # self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N) self.solver.reset() - # self.solver.options_set('print_level', 2) + + self.x_sol = np.zeros((N+1, X_DIM)) + self.u_sol = np.zeros((N, 1)) self.v_solution = np.zeros(N+1) self.a_solution = np.zeros(N+1) - self.prev_a = np.array(self.a_solution) self.j_solution = np.zeros(N) + self.a_prev = np.array(self.a_solution) self.yref = np.zeros((N+1, COST_DIM)) + for i in range(N): self.solver.cost_set(i, "yref", self.yref[i]) self.solver.cost_set(N, "yref", self.yref[N][:COST_E_DIM]) - self.x_sol = np.zeros((N+1, X_DIM)) - self.u_sol = np.zeros((N,1)) + self.params = np.zeros((N+1, PARAM_DIM)) for i in range(N+1): self.solver.set(i, 'x', np.zeros(X_DIM)) + self.last_cloudlog_t = 0 self.status = False self.crash_cnt = 0.0 @@ -276,16 +269,9 @@ class LongitudinalMpc: def set_weights(self, prev_accel_constraint=True, personality=log.LongitudinalPersonality.standard): jerk_factor = get_jerk_factor(personality) - if self.mode == 'acc': - a_change_cost = A_CHANGE_COST if prev_accel_constraint else 0 - cost_weights = [X_EGO_OBSTACLE_COST, X_EGO_COST, V_EGO_COST, A_EGO_COST, jerk_factor * a_change_cost, jerk_factor * J_EGO_COST] - constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST] - elif self.mode == 'blended': - a_change_cost = 40.0 if prev_accel_constraint else 0 - cost_weights = [0., 0.1, 0.2, 5.0, a_change_cost, 1.0] - constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST] - else: - raise NotImplementedError(f'Planner mode {self.mode} not recognized in planner cost set') + a_change_cost = A_CHANGE_COST if prev_accel_constraint else 0 + cost_weights = [X_EGO_OBSTACLE_COST, X_EGO_COST, V_EGO_COST, A_EGO_COST, jerk_factor * a_change_cost, jerk_factor * J_EGO_COST] + constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST] self.set_cost_weights(cost_weights, constraint_cost_weights) def set_cur_state(self, v, a): @@ -320,14 +306,14 @@ class LongitudinalMpc: # MPC will not converge if immediate crash is expected # Clip lead distance to what is still possible to brake for - min_x_lead = ((v_ego + v_lead)/2) * (v_ego - v_lead) / (-ACCEL_MIN * 2) + min_x_lead = MIN_X_LEAD_FACTOR * (v_ego + v_lead) * (v_ego - v_lead) / (-ACCEL_MIN * 2) x_lead = np.clip(x_lead, min_x_lead, 1e8) v_lead = np.clip(v_lead, 0.0, 1e8) a_lead = np.clip(a_lead, -10., 5.) lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau) return lead_xv - def update(self, radarstate, v_cruise, x, v, a, j, personality=log.LongitudinalPersonality.standard): + def update(self, radarstate, v_cruise, personality=log.LongitudinalPersonality.standard): t_follow = get_T_FOLLOW(personality) v_ego = self.x0[1] self.status = radarstate.leadOne.status or radarstate.leadTwo.status @@ -341,56 +327,28 @@ class LongitudinalMpc: lead_0_obstacle = lead_xv_0[:,0] + get_stopped_equivalence_factor(lead_xv_0[:,1]) lead_1_obstacle = lead_xv_1[:,0] + get_stopped_equivalence_factor(lead_xv_1[:,1]) - self.params[:,0] = ACCEL_MIN - self.params[:,1] = ACCEL_MAX + # Fake an obstacle for cruise, this ensures smooth acceleration to set speed + # when the leads are no factor. + v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05) + # TODO does this make sense when max_a is negative? + v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05) + v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), v_lower, v_upper) + cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow) - # Update in ACC mode or ACC/e2e blend - if self.mode == 'acc': - self.params[:,5] = LEAD_DANGER_FACTOR + x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle]) + self.source = MPC_SOURCES[np.argmin(x_obstacles[0])] - # Fake an obstacle for cruise, this ensures smooth acceleration to set speed - # when the leads are no factor. - v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05) - # TODO does this make sense when max_a is negative? - v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05) - v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), - v_lower, - v_upper) - cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow) - x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle]) - self.source = SOURCES[np.argmin(x_obstacles[0])] - - # These are not used in ACC mode - x[:], v[:], a[:], j[:] = 0.0, 0.0, 0.0, 0.0 - - elif self.mode == 'blended': - self.params[:,5] = 1.0 - - x_obstacles = np.column_stack([lead_0_obstacle, - lead_1_obstacle]) - cruise_target = T_IDXS * np.clip(v_cruise, v_ego - 2.0, 1e3) + x[0] - xforward = ((v[1:] + v[:-1]) / 2) * (T_IDXS[1:] - T_IDXS[:-1]) - x = np.cumsum(np.insert(xforward, 0, x[0])) - - x_and_cruise = np.column_stack([x, cruise_target]) - x = np.min(x_and_cruise, axis=1) - - self.source = 'e2e' if x_and_cruise[1,0] < x_and_cruise[1,1] else 'cruise' - - else: - raise NotImplementedError(f'Planner mode {self.mode} not recognized in planner update') - - self.yref[:,1] = x - self.yref[:,2] = v - self.yref[:,3] = a - self.yref[:,5] = j + self.yref[:,:] = 0.0 for i in range(N): self.solver.set(i, "yref", self.yref[i]) self.solver.set(N, "yref", self.yref[N][:COST_E_DIM]) + self.params[:,0] = ACCEL_MIN + self.params[:,1] = ACCEL_MAX self.params[:,2] = np.min(x_obstacles, axis=1) - self.params[:,3] = np.copy(self.prev_a) + self.params[:,3] = np.copy(self.a_prev) self.params[:,4] = t_follow + self.params[:,5] = LEAD_DANGER_FACTOR self.run() if (np.any(lead_xv_0[FCW_IDXS,0] - self.x_sol[FCW_IDXS,0] < CRASH_DISTANCE) and @@ -399,18 +357,7 @@ class LongitudinalMpc: else: self.crash_cnt = 0 - # Check if it got within lead comfort range - # TODO This should be done cleaner - if self.mode == 'blended': - if any((lead_0_obstacle - get_safe_obstacle_distance(self.x_sol[:,1], t_follow))- self.x_sol[:,0] < 0.0): - self.source = 'lead0' - if any((lead_1_obstacle - get_safe_obstacle_distance(self.x_sol[:,1], t_follow))- self.x_sol[:,0] < 0.0) and \ - (lead_1_obstacle[0] - lead_0_obstacle[0]): - self.source = 'lead1' - def run(self): - # t0 = time.monotonic() - # reset = 0 for i in range(N+1): self.solver.set(i, 'p', self.params[i]) self.solver.constraints_set(0, "lbx", self.x0) @@ -422,13 +369,6 @@ class LongitudinalMpc: self.time_linearization = float(self.solver.get_stats('time_lin')[0]) self.time_integrator = float(self.solver.get_stats('time_sim')[0]) - # qp_iter = self.solver.get_stats('statistics')[-1][-1] # SQP_RTI specific - # print(f"long_mpc timings: tot {self.solve_time:.2e}, qp {self.time_qp_solution:.2e}, lin {self.time_linearization:.2e}, \ - # integrator {self.time_integrator:.2e}, qp_iter {qp_iter}") - # res = self.solver.get_residuals() - # print(f"long_mpc residuals: {res[0]:.2e}, {res[1]:.2e}, {res[2]:.2e}, {res[3]:.2e}") - # self.solver.print_statistics() - for i in range(N+1): self.x_sol[i] = self.solver.get(i, 'x') for i in range(N): @@ -438,7 +378,7 @@ class LongitudinalMpc: self.a_solution = self.x_sol[:,2] self.j_solution = self.u_sol[:,0] - self.prev_a = np.interp(T_IDXS + self.dt, T_IDXS, self.a_solution) + self.a_prev = np.interp(T_IDXS + self.dt, T_IDXS, self.a_solution) t = time.monotonic() if self.solution_status != 0: @@ -446,12 +386,8 @@ class LongitudinalMpc: self.last_cloudlog_t = t cloudlog.warning(f"Long mpc reset, solution_status: {self.solution_status}") self.reset() - # reset = 1 - # print(f"long_mpc timings: total internal {self.solve_time:.2e}, external: {(time.monotonic() - t0):.2e} qp {self.time_qp_solution:.2e}, \ - # lin {self.time_linearization:.2e} qp_iter {qp_iter}, reset {reset}") if __name__ == "__main__": ocp = gen_long_ocp() AcadosOcpSolver.generate(ocp, json_file=JSON_FILE) - # AcadosOcpSolver.build(ocp.code_export_directory, with_cython=True) diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 0501f669f1..e02b02d2e0 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -9,7 +9,7 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState -from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc +from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, LongitudinalPlanSource 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_accel_from_plan from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET @@ -17,7 +17,6 @@ from openpilot.common.swaglog import cloudlog from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlannerSP -LON_MPC_STEP = 0.2 # first step is 0.2s A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6] A_CRUISE_MAX_BP = [0., 10.0, 25., 40.] CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N] @@ -28,14 +27,12 @@ MIN_ALLOW_THROTTLE_SPEED = 2.5 _A_TOTAL_MAX_V = [1.7, 3.2] _A_TOTAL_MAX_BP = [20., 40.] - def get_max_accel(v_ego): return np.interp(v_ego, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS) def get_coast_accel(pitch): return np.sin(pitch) * -5.65 - 0.3 # fitted from data using xx/projects/allow_throttle/compute_coast_accel.py - def limit_accel_in_turns(v_ego, angle_steers, a_target, CP): """ This function returns a limited long acceleration allowed, depending on the existing lateral acceleration @@ -54,8 +51,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP): def __init__(self, CP, CP_SP, 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, CP_SP, self.mpc) self.fcw = False self.dt = dt @@ -70,7 +65,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP): self.v_desired_trajectory = np.zeros(CONTROL_N) self.a_desired_trajectory = np.zeros(CONTROL_N) self.j_desired_trajectory = np.zeros(CONTROL_N) - self.solverExecutionTime = 0.0 @staticmethod def parse_model(model_msg): @@ -93,14 +87,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): return x, v, a, j, throttle_prob def update(self, sm): - mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc' - if not self.mlsim: - self.mpc.mode = mode LongitudinalPlannerSP.update(self, sm) - if dec_mpc_mode := self.get_mpc_mode(): - 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]) @@ -123,12 +110,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP): # No change cost when user is controlling the speed, or when standstill prev_accel_constraint = not (reset_state or sm['carState'].standstill) - if mode == 'acc': - accel_clip = [ACCEL_MIN, get_max_accel(v_ego)] - steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg - accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP) - else: - accel_clip = [ACCEL_MIN, ACCEL_MAX] + accel_clip = [ACCEL_MIN, get_max_accel(v_ego)] + steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg + accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP) if reset_state: self.v_desired_filter.x = v_ego @@ -137,7 +121,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)) - x, v, a, j, throttle_prob = self.parse_model(sm['modelV2']) + _, _, _, _, 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 @@ -154,7 +138,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality) self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired) - self.mpc.update(sm['radarState'], v_cruise, x, v, a, j, personality=sm['selfdriveState'].personality) + self.mpc.update(sm['radarState'], v_cruise, personality=sm['selfdriveState'].personality) self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution) self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution) @@ -176,12 +160,14 @@ class LongitudinalPlanner(LongitudinalPlannerSP): output_a_target_e2e = sm['modelV2'].action.desiredAcceleration output_should_stop_e2e = sm['modelV2'].action.shouldStop - if mode == 'acc' or not self.mlsim: + if self.is_e2e(sm): + output_a_target = min(output_a_target_e2e, output_a_target_mpc) + self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc + if output_a_target < output_a_target_mpc: + self.mpc.source = LongitudinalPlanSource.e2e + else: output_a_target = output_a_target_mpc self.output_should_stop = output_should_stop_mpc - else: - output_a_target = min(output_a_target_mpc, output_a_target_e2e) - self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc for idx in range(2): accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05) diff --git a/selfdrive/controls/tests/test_following_distance.py b/selfdrive/controls/tests/test_following_distance.py index 0fd543dd60..8f66d89bf8 100644 --- a/selfdrive/controls/tests/test_following_distance.py +++ b/selfdrive/controls/tests/test_following_distance.py @@ -4,10 +4,15 @@ from parameterized import parameterized_class from cereal import log -from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import desired_follow_distance, get_T_FOLLOW +from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import get_safe_obstacle_distance, get_stopped_equivalence_factor, get_T_FOLLOW from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver +def desired_follow_distance(v_ego, v_lead, t_follow=None): + if t_follow is None: + t_follow = get_T_FOLLOW() + return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead) + def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personality=0): man = Maneuver( '', diff --git a/selfdrive/controls/tests/test_latcontrol_torque_buffer.py b/selfdrive/controls/tests/test_latcontrol_torque_buffer.py new file mode 100644 index 0000000000..52810ff55b --- /dev/null +++ b/selfdrive/controls/tests/test_latcontrol_torque_buffer.py @@ -0,0 +1,47 @@ +from parameterized import parameterized + +from cereal import car, log +from opendbc.car.car_helpers import interfaces +from opendbc.car.toyota.values import CAR as TOYOTA +from opendbc.car.vehicle_model import VehicleModel +from openpilot.common.realtime import DT_CTRL +from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque, LAT_ACCEL_REQUEST_BUFFER_SECONDS + +from openpilot.selfdrive.car.helpers import convert_to_capnp +from openpilot.selfdrive.locationd.helpers import Pose +from openpilot.common.mock.generators import generate_livePose +from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces + +def get_controller(car_name): + CarInterface = interfaces[car_name] + CP = CarInterface.get_non_essential_params(car_name) + CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) + CI = CarInterface(CP, CP_SP) + sunnypilot_interfaces.setup_interfaces(CI) + CP_SP = convert_to_capnp(CP_SP) + VM = VehicleModel(CP) + controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI, DT_CTRL) + return controller, VM + +class TestLatControlTorqueBuffer: + + @parameterized.expand([(TOYOTA.TOYOTA_COROLLA_TSS2,)]) + def test_request_buffer_consistency(self, car_name): + buffer_steps = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / DT_CTRL) + controller, VM = get_controller(car_name) + + CS = car.CarState.new_message() + CS.vEgo = 30 + CS.steeringPressed = False + params = log.LiveParametersData.new_message() + + lp = generate_livePose() + pose = Pose.from_live_pose(lp.livePose) + + for _ in range(buffer_steps): + controller.update(True, CS, VM, params, False, 0.001, pose, False, 0.2) + assert all(val != 0 for val in controller.lat_accel_request_buffer) + + for _ in range(buffer_steps): + controller.update(False, CS, VM, params, False, 0.0, pose, False, 0.2) + assert all(val == 0 for val in controller.lat_accel_request_buffer) diff --git a/selfdrive/debug/mem_usage.py b/selfdrive/debug/mem_usage.py new file mode 100755 index 0000000000..9bed566e19 --- /dev/null +++ b/selfdrive/debug/mem_usage.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +import argparse +import os +from collections import defaultdict + +import numpy as np +from tabulate import tabulate + +from openpilot.tools.lib.logreader import LogReader + +DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" +MB = 1024 * 1024 +TABULATE_OPTS = dict(tablefmt="simple_grid", stralign="center", numalign="center") + + +def _get_procs(): + from openpilot.selfdrive.test.test_onroad import PROCS + return PROCS + + +def is_openpilot_proc(name): + if any(p in name for p in _get_procs()): + return True + # catch openpilot processes not in PROCS (athenad, manager, etc.) + return 'openpilot' in name or name.startswith(('selfdrive.', 'system.')) + + +def get_proc_name(proc): + if len(proc.cmdline) > 0: + return list(proc.cmdline)[0] + return proc.name + + +def pct(val_mb, total_mb): + return val_mb / total_mb * 100 if total_mb else 0 + + +def has_pss(proc_logs): + """Check if logs contain PSS data (new field, not in old logs).""" + try: + for proc in proc_logs[-1].procLog.procs: + if proc.memPss > 0: + return True + except AttributeError: + pass + return False + + +def print_summary(proc_logs, device_states): + mem = proc_logs[-1].procLog.mem + total = mem.total / MB + used = (mem.total - mem.available) / MB + cached = mem.cached / MB + shared = mem.shared / MB + buffers = mem.buffers / MB + + lines = [ + f" Total: {total:.0f} MB", + f" Used (total-avail): {used:.0f} MB ({pct(used, total):.0f}%)", + f" Cached: {cached:.0f} MB ({pct(cached, total):.0f}%) Buffers: {buffers:.0f} MB ({pct(buffers, total):.0f}%)", + f" Shared/MSGQ: {shared:.0f} MB ({pct(shared, total):.0f}%)", + ] + + if device_states: + mem_pcts = [m.deviceState.memoryUsagePercent for m in device_states] + lines.append(f" deviceState memory: {np.min(mem_pcts)}-{np.max(mem_pcts)}% (avg {np.mean(mem_pcts):.0f}%)") + + print("\n-- Memory Summary --") + print("\n".join(lines)) + return total + + +def collect_per_process_mem(proc_logs, use_pss): + """Collect per-process memory samples. Returns {name: {metric: [values_per_sample_in_MB]}}.""" + by_proc = defaultdict(lambda: defaultdict(list)) + + for msg in proc_logs: + sample = defaultdict(lambda: defaultdict(float)) + for proc in msg.procLog.procs: + name = get_proc_name(proc) + sample[name]['rss'] += proc.memRss / MB + if use_pss: + sample[name]['pss'] += proc.memPss / MB + sample[name]['pss_anon'] += proc.memPssAnon / MB + sample[name]['pss_shmem'] += proc.memPssShmem / MB + + for name, metrics in sample.items(): + for metric, val in metrics.items(): + by_proc[name][metric].append(val) + + return by_proc + + +def _has_pss_detail(by_proc) -> bool: + """Check if any process has non-zero pss_anon/pss_shmem (unavailable on some kernels).""" + return any(sum(v.get('pss_anon', [])) > 0 or sum(v.get('pss_shmem', [])) > 0 for v in by_proc.values()) + + +def process_table_rows(by_proc, total_mb, use_pss, show_detail): + """Build table rows. Returns (rows, total_row).""" + mem_key = 'pss' if use_pss else 'rss' + rows = [] + for name in sorted(by_proc, key=lambda n: np.mean(by_proc[n][mem_key]), reverse=True): + m = by_proc[name] + vals = m[mem_key] + avg = round(np.mean(vals)) + row = [name, f"{avg} MB", f"{round(np.max(vals))} MB", f"{round(pct(avg, total_mb), 1)}%"] + if show_detail: + row.append(f"{round(np.mean(m['pss_anon']))} MB") + row.append(f"{round(np.mean(m['pss_shmem']))} MB") + rows.append(row) + + # Total row + total_row = None + if by_proc: + max_samples = max(len(v[mem_key]) for v in by_proc.values()) + totals = [] + for i in range(max_samples): + s = sum(v[mem_key][i] for v in by_proc.values() if i < len(v[mem_key])) + totals.append(s) + avg_total = round(np.mean(totals)) + total_row = ["TOTAL", f"{avg_total} MB", f"{round(np.max(totals))} MB", f"{round(pct(avg_total, total_mb), 1)}%"] + if show_detail: + total_row.append(f"{round(sum(np.mean(v['pss_anon']) for v in by_proc.values()))} MB") + total_row.append(f"{round(sum(np.mean(v['pss_shmem']) for v in by_proc.values()))} MB") + + return rows, total_row + + +def print_process_tables(op_procs, other_procs, total_mb, use_pss): + all_procs = {**op_procs, **other_procs} + show_detail = use_pss and _has_pss_detail(all_procs) + + header = ["process", "avg", "max", "%"] + if show_detail: + header += ["anon", "shmem"] + + op_rows, op_total = process_table_rows(op_procs, total_mb, use_pss, show_detail) + # filter other: >5MB avg and not bare interpreter paths (test infra noise) + other_filtered = {n: v for n, v in other_procs.items() + if np.mean(v['pss' if use_pss else 'rss']) > 5.0 + and os.path.basename(n.split()[0]) not in ('python', 'python3')} + other_rows, other_total = process_table_rows(other_filtered, total_mb, use_pss, show_detail) + + rows = op_rows + if op_total: + rows.append(op_total) + if other_rows: + sep_width = len(header) + rows.append([""] * sep_width) + rows.extend(other_rows) + if other_total: + other_total[0] = "TOTAL (other)" + rows.append(other_total) + + metric = "PSS (no shared double-count)" if use_pss else "RSS (includes shared, overcounts)" + print(f"\n-- Per-Process Memory: {metric} --") + print(tabulate(rows, header, **TABULATE_OPTS)) + + +def print_memory_accounting(proc_logs, op_procs, other_procs, total_mb, use_pss): + last = proc_logs[-1].procLog.mem + used = (last.total - last.available) / MB + shared = last.shared / MB + cached_buf = (last.buffers + last.cached) / MB - shared # shared (MSGQ) is in Cached; separate it + msgq = shared + + mem_key = 'pss' if use_pss else 'rss' + op_total = sum(v[mem_key][-1] for v in op_procs.values()) if op_procs else 0 + other_total = sum(v[mem_key][-1] for v in other_procs.values()) if other_procs else 0 + proc_sum = op_total + other_total + remainder = used - (cached_buf + msgq) - proc_sum + + if not use_pss: + # RSS double-counts shared; add back once to partially correct + remainder += shared + + header = ["", "MB", "%", ""] + label = "PSS" if use_pss else "RSS*" + rows = [ + ["Used (total - avail)", f"{used:.0f}", f"{pct(used, total_mb):.1f}", "memory in use by the system"], + [" Cached + Buffers", f"{cached_buf:.0f}", f"{pct(cached_buf, total_mb):.1f}", "pagecache + fs metadata, reclaimable"], + [" MSGQ (shared)", f"{msgq:.0f}", f"{pct(msgq, total_mb):.1f}", "/dev/shm tmpfs, also in process PSS"], + [f" openpilot {label}", f"{op_total:.0f}", f"{pct(op_total, total_mb):.1f}", "sum of openpilot process memory"], + [f" other {label}", f"{other_total:.0f}", f"{pct(other_total, total_mb):.1f}", "sum of non-openpilot process memory"], + [" kernel/ION/GPU", f"{remainder:.0f}", f"{pct(remainder, total_mb):.1f}", "slab, ION/DMA-BUF, GPU, page tables"], + ] + note = "" if use_pss else " (*RSS overcounts shared mem)" + print(f"\n-- Memory Accounting (last sample){note} --") + print(tabulate(rows, header, tablefmt="simple_grid", stralign="right")) + + +def print_report(proc_logs, device_states=None): + """Print full memory analysis report. Can be called from tests or CLI.""" + if not proc_logs: + print("No procLog messages found") + return + + print(f"{len(proc_logs)} procLog samples, {len(device_states or [])} deviceState samples") + + use_pss = has_pss(proc_logs) + if not use_pss: + print(" (no PSS data — re-record with updated proclogd for accurate numbers)") + + total_mb = print_summary(proc_logs, device_states or []) + + by_proc = collect_per_process_mem(proc_logs, use_pss) + op_procs = {n: v for n, v in by_proc.items() if is_openpilot_proc(n)} + other_procs = {n: v for n, v in by_proc.items() if not is_openpilot_proc(n)} + + print_process_tables(op_procs, other_procs, total_mb, use_pss) + print_memory_accounting(proc_logs, op_procs, other_procs, total_mb, use_pss) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Analyze memory usage from route logs") + parser.add_argument("route", nargs="?", default=None, help="route ID or local rlog path") + parser.add_argument("--demo", action="store_true", help=f"use demo route ({DEMO_ROUTE})") + args = parser.parse_args() + + if args.demo: + route = DEMO_ROUTE + elif args.route: + route = args.route + else: + parser.error("provide a route or use --demo") + + print(f"Reading logs from: {route}") + + proc_logs = [] + device_states = [] + for msg in LogReader(route): + if msg.which() == 'procLog': + proc_logs.append(msg) + elif msg.which() == 'deviceState': + device_states.append(msg) + + print_report(proc_logs, device_states) diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 9bb8b68dc3..7865e86f66 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -29,7 +29,7 @@ for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transfor cython_libs = envCython["LIBS"] + libs commonmodel_lib = lenv.Library('commonmodel', common_src) lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) -tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x] +tinygrad_files = sorted(["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x]) # Get model metadata for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: @@ -50,7 +50,7 @@ def tg_compile(flags, model_name): # Compile small models for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: flags = { - 'larch64': 'DEV=QCOM', + 'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 IMAGE=2 JIT_BATCH_SIZE=0', 'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env }.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0') tg_compile(flags, model_name) diff --git a/selfdrive/modeld/dmonitoringmodeld.py b/selfdrive/modeld/dmonitoringmodeld.py index fca762c69b..7177998571 100755 --- a/selfdrive/modeld/dmonitoringmodeld.py +++ b/selfdrive/modeld/dmonitoringmodeld.py @@ -59,7 +59,7 @@ class ModelState: self.tensor_inputs['input_img'] = Tensor(self.frame.buffer_from_cl(input_img_cl).reshape(self.input_shapes['input_img']), dtype=dtypes.uint8).realize() - output = self.model_run(**self.tensor_inputs).contiguous().realize().uop.base.buffer.numpy() + output = self.model_run(**self.tensor_inputs).numpy().flatten() t2 = time.perf_counter() return output, t2 - t1 diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index 6ffce3c5b4..216c5da443 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -217,7 +217,7 @@ class ModelState(ModelStateBase): self.numpy_inputs[k][:] = self.full_input_queues.get(k)[k] self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention'] - self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy() + self.policy_output = self.policy_run(**self.policy_inputs).numpy().flatten() policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices)) combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict} diff --git a/selfdrive/modeld/models/driving_policy.onnx b/selfdrive/modeld/models/driving_policy.onnx index 92c81954d2..611ae9fe85 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:1edea5bb56f876db4cec97c150799513f6a59373f3ad152d55e4dcaab1b809e3 -size 13926324 +oid sha256:78477124cbf3ffe30fa951ebada8410b43c4242c6054584d656f1d329b067e15 +size 14060847 diff --git a/selfdrive/modeld/models/driving_vision.onnx b/selfdrive/modeld/models/driving_vision.onnx index 76c96670a9..6c9fc4c84d 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:1dc66bc06f250b577653ccbeaa2c6521b3d46749f601d0a1a366419e929ca438 -size 46271942 +oid sha256:ee29ee5bce84d1ce23e9ff381280de9b4e4d96d2934cd751740354884e112c66 +size 46877473 diff --git a/selfdrive/pandad/SConscript b/selfdrive/pandad/SConscript index 58777cafe9..5e0b782c1e 100644 --- a/selfdrive/pandad/SConscript +++ b/selfdrive/pandad/SConscript @@ -1,13 +1,9 @@ -Import('env', 'envCython', 'common', 'messaging') +Import('env', 'common', 'messaging') libs = ['usb-1.0', common, messaging, 'pthread'] panda = env.Library('panda', ['panda.cc', 'panda_comms.cc', 'spi.cc']) env.Program('pandad', ['main.cc', 'pandad.cc', 'panda_safety.cc'], LIBS=[panda] + libs) -env.Library('libcan_list_to_can_capnp', ['can_list_to_can_capnp.cc']) - -pandad_python = envCython.Program('pandad_api_impl.so', 'pandad_api_impl.pyx', LIBS=["can_list_to_can_capnp", 'capnp', 'kj'] + envCython["LIBS"]) -Export('pandad_python') if GetOption('extras'): env.Program('tests/test_pandad_usbprotocol', ['tests/test_pandad_usbprotocol.cc'], LIBS=[panda] + libs) diff --git a/selfdrive/pandad/__init__.py b/selfdrive/pandad/__init__.py index cc680e1676..0c17e886a2 100644 --- a/selfdrive/pandad/__init__.py +++ b/selfdrive/pandad/__init__.py @@ -1,4 +1,3 @@ -# Cython, now uses scons to build from openpilot.selfdrive.pandad.pandad_api_impl import can_list_to_can_capnp, can_capnp_to_list assert can_list_to_can_capnp assert can_capnp_to_list diff --git a/selfdrive/pandad/can_list_to_can_capnp.cc b/selfdrive/pandad/can_list_to_can_capnp.cc deleted file mode 100644 index f2cf153453..0000000000 --- a/selfdrive/pandad/can_list_to_can_capnp.cc +++ /dev/null @@ -1,50 +0,0 @@ -#include "cereal/messaging/messaging.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; - auto event = msg.initEvent(valid); - - auto canData = sendcan ? event.initSendcan(can_list.size()) : event.initCan(can_list.size()); - int j = 0; - for (auto it = can_list.begin(); it != can_list.end(); it++, j++) { - auto c = canData[j]; - c.setAddress(it->address); - c.setDat(kj::arrayPtr((uint8_t*)it->dat.data(), it->dat.size())); - c.setSrc(it->src); - } - const uint64_t msg_size = capnp::computeSerializedSizeInWords(msg) * sizeof(capnp::word); - out.resize(msg_size); - kj::ArrayOutputStream output_stream(kj::ArrayPtr((unsigned char *)out.data(), msg_size)); - capnp::writeMessage(output_stream, msg); -} - -// Converts a vector of Cap'n Proto serialized can strings into a vector of CanData structures. -void can_capnp_to_can_list_cpp(const std::vector &strings, std::vector &can_list, bool sendcan) { - AlignedBuffer aligned_buf; - can_list.reserve(strings.size()); - - for (const auto &str : strings) { - // extract the messages - capnp::FlatArrayMessageReader reader(aligned_buf.align(str.data(), str.size())); - cereal::Event::Reader event = reader.getRoot(); - - auto frames = sendcan ? event.getSendcan() : event.getCan(); - - // Add new CanData entry - CanData &can_data = can_list.emplace_back(); - can_data.nanos = event.getLogMonoTime(); - can_data.frames.reserve(frames.size()); - - // Populate CAN frames - for (const auto &frame : frames) { - CanFrame &can_frame = can_data.frames.emplace_back(); - can_frame.src = frame.getSrc(); - can_frame.address = frame.getAddress(); - - // Copy CAN data - auto dat = frame.getDat(); - can_frame.dat.assign(dat.begin(), dat.end()); - } - } -} diff --git a/selfdrive/pandad/can_types.h b/selfdrive/pandad/can_types.h deleted file mode 100644 index 5fae581cfa..0000000000 --- a/selfdrive/pandad/can_types.h +++ /dev/null @@ -1,15 +0,0 @@ -#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/pandad.py b/selfdrive/pandad/pandad.py index f4064ddcd4..d21e60f838 100755 --- a/selfdrive/pandad/pandad.py +++ b/selfdrive/pandad/pandad.py @@ -6,16 +6,16 @@ import time import signal import subprocess -from panda import Panda, PandaDFU, PandaProtocolMismatch, FW_PATH +from panda import Panda, PandaDFU, PandaProtocolMismatch, McuType, FW_PATH from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.system.hardware import HARDWARE from openpilot.common.swaglog import cloudlog -def get_expected_signature(panda: Panda) -> bytes: +def get_expected_signature() -> bytes: try: - fn = os.path.join(FW_PATH, panda.get_mcu_type().config.app_fn) + fn = os.path.join(FW_PATH, McuType.H7.config.app_fn) return Panda.get_signature_from_firmware(fn) except Exception: cloudlog.exception("Error computing expected signature") @@ -35,7 +35,7 @@ def flash_panda(panda_serial: str) -> Panda: cloudlog.warning(f"Panda {panda_serial} is not supported (hw_type: {panda.get_type()}), skipping flash...") return panda - fw_signature = get_expected_signature(panda) + fw_signature = get_expected_signature() internal_panda = panda.is_internal() panda_version = "bootstub" if panda.bootstub else panda.get_version() diff --git a/selfdrive/pandad/pandad_api_impl.py b/selfdrive/pandad/pandad_api_impl.py new file mode 100644 index 0000000000..75a7ba484e --- /dev/null +++ b/selfdrive/pandad/pandad_api_impl.py @@ -0,0 +1,88 @@ +import time +from cereal import log + +NO_TRAVERSAL_LIMIT = 2**64 - 1 + +# Cache schema fields for faster access (avoids string lookup on each field access) +_cached_reader_fields = None # (address_field, dat_field, src_field) for reading +_cached_writer_fields = None # (address_field, dat_field, src_field) for writing + + +def _get_reader_fields(schema): + """Get cached schema field objects for reading.""" + global _cached_reader_fields + if _cached_reader_fields is None: + fields = schema.fields + _cached_reader_fields = (fields['address'], fields['dat'], fields['src']) + return _cached_reader_fields + + +def _get_writer_fields(schema): + """Get cached schema field objects for writing.""" + global _cached_writer_fields + if _cached_writer_fields is None: + fields = schema.fields + _cached_writer_fields = (fields['address'], fields['dat'], fields['src']) + return _cached_writer_fields + + +def can_list_to_can_capnp(can_msgs, msgtype='can', valid=True): + """Convert list of CAN messages to Cap'n Proto serialized bytes. + + Args: + can_msgs: List of tuples [(address, data_bytes, src), ...] + msgtype: 'can' or 'sendcan' + valid: Whether the event is valid + + Returns: + Cap'n Proto serialized bytes + """ + global _cached_writer_fields + + dat = log.Event.new_message(valid=valid, logMonoTime=int(time.monotonic() * 1e9)) + can_data = dat.init(msgtype, len(can_msgs)) + + # Cache schema fields on first call + if _cached_writer_fields is None and len(can_msgs) > 0: + _cached_writer_fields = _get_writer_fields(can_data[0].schema) + + if _cached_writer_fields is not None: + addr_f, dat_f, src_f = _cached_writer_fields + for i, msg in enumerate(can_msgs): + f = can_data[i] + f._set_by_field(addr_f, msg[0]) + f._set_by_field(dat_f, msg[1]) + f._set_by_field(src_f, msg[2]) + + return dat.to_bytes() + + +def can_capnp_to_list(strings, msgtype='can'): + """Convert Cap'n Proto serialized bytes to list of CAN messages. + + Args: + strings: Tuple/list of serialized Cap'n Proto bytes + msgtype: 'can' or 'sendcan' + + Returns: + List of tuples [(nanos, [(address, data, src), ...]), ...] + """ + global _cached_reader_fields + result = [] + + for s in strings: + with log.Event.from_bytes(s, traversal_limit_in_words=NO_TRAVERSAL_LIMIT) as event: + frames = getattr(event, msgtype) + + # Cache schema fields on first frame for faster access + if _cached_reader_fields is None and len(frames) > 0: + _cached_reader_fields = _get_reader_fields(frames[0].schema) + + if _cached_reader_fields is not None: + addr_f, dat_f, src_f = _cached_reader_fields + frame_list = [(f._get_by_field(addr_f), f._get_by_field(dat_f), f._get_by_field(src_f)) for f in frames] + else: + frame_list = [] + + result.append((event.logMonoTime, frame_list)) + return result diff --git a/selfdrive/pandad/pandad_api_impl.pyx b/selfdrive/pandad/pandad_api_impl.pyx deleted file mode 100644 index aaecb8a594..0000000000 --- a/selfdrive/pandad/pandad_api_impl.pyx +++ /dev/null @@ -1,56 +0,0 @@ -# distutils: language = c++ -# cython: language_level=3 -from cython.operator cimport dereference as deref, preincrement as preinc -from libcpp.vector cimport vector -from libcpp.string cimport string -from libcpp cimport bool -from libc.stdint cimport uint8_t, uint32_t, uint64_t - -cdef extern from "selfdrive/pandad/can_types.h": - cdef struct CanFrame: - long src - uint32_t address - vector[uint8_t] dat - - cdef struct CanData: - uint64_t nanos - vector[CanFrame] frames - -cdef extern from "can_list_to_can_capnp.cc": - void can_list_to_can_capnp_cpp(const vector[CanFrame] &can_list, string &out, bool sendcan, bool valid) nogil - void can_capnp_to_can_list_cpp(const vector[string] &strings, vector[CanData] &can_data, bool sendcan) - -def can_list_to_can_capnp(can_msgs, msgtype='can', valid=True): - cdef CanFrame *f - cdef vector[CanFrame] can_list - cdef uint32_t cpp_can_msgs_len = len(can_msgs) - - with nogil: - can_list.reserve(cpp_can_msgs_len) - - for can_msg in can_msgs: - f = &(can_list.emplace_back()) - f.address = can_msg[0] - f.dat = can_msg[1] - f.src = can_msg[2] - - cdef string out - cdef bool is_sendcan = (msgtype == 'sendcan') - cdef bool is_valid = valid - with nogil: - can_list_to_can_capnp_cpp(can_list, out, is_sendcan, is_valid) - return out - -def can_capnp_to_list(strings, msgtype='can'): - cdef vector[CanData] data - can_capnp_to_can_list_cpp(strings, data, msgtype == 'sendcan') - - result = [] - cdef CanData *d - cdef vector[CanData].iterator it = data.begin() - while it != data.end(): - d = &deref(it) - frames = [(f.address, (&f.dat[0])[:f.dat.size()], f.src) for f in d.frames] - result.append((d.nanos, frames)) - preinc(it) - return result diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index 7e8c553ad3..62f4553bff 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -304,11 +304,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { }, EventName.stockLkas: { - ET.PERMANENT: Alert( - "TAKE CONTROL", - "Stock LKAS: Lane Departure Detected", - AlertStatus.critical, AlertSize.full, - Priority.HIGH, VisualAlert.fcw, AudibleAlert.none, 2.), ET.NO_ENTRY: NoEntryAlert("Stock LKAS: Lane Departure Detected"), }, diff --git a/selfdrive/test/longitudinal_maneuvers/maneuver.py b/selfdrive/test/longitudinal_maneuvers/maneuver.py index dfd5b3e109..ba0379f2d7 100644 --- a/selfdrive/test/longitudinal_maneuvers/maneuver.py +++ b/selfdrive/test/longitudinal_maneuvers/maneuver.py @@ -60,7 +60,8 @@ class Maneuver: log['distance_lead'], log['speed'], speed_lead, - log['acceleration']])) + log['acceleration'], + log['d_rel']])) if d_rel < .4 and (self.only_radar or prob_lead > 0.5): print("Crashed!!!!") diff --git a/selfdrive/test/process_replay/README.md b/selfdrive/test/process_replay/README.md index dc801e4285..28f3b7cd2a 100644 --- a/selfdrive/test/process_replay/README.md +++ b/selfdrive/test/process_replay/README.md @@ -5,7 +5,7 @@ Process replay is a regression test designed to identify any changes in the outp If the test fails, make sure that you didn't unintentionally change anything. If there are intentional changes, the reference logs will be updated. Use `test_processes.py` to run the test locally. -Use `FILEREADER_CACHE='1' test_processes.py` to cache log files. +Log files are cached by default. Use `DISABLE_FILEREADER_CACHE='1' test_processes.py` to disable caching. Currently the following processes are tested: @@ -22,7 +22,7 @@ Currently the following processes are tested: ### Usage ``` Usage: test_processes.py [-h] [--whitelist-procs PROCS] [--whitelist-cars CARS] [--blacklist-procs PROCS] - [--blacklist-cars CARS] [--ignore-fields FIELDS] [--ignore-msgs MSGS] [--update-refs] [--upload-only] + [--blacklist-cars CARS] [--ignore-fields FIELDS] [--ignore-msgs MSGS] [--update-refs] Regression test to identify changes in a process's output optional arguments: -h, --help show this help message and exit @@ -33,7 +33,6 @@ optional arguments: --ignore-fields IGNORE_FIELDS Extra fields or msgs to ignore (e.g. driverMonitoringState.events) --ignore-msgs IGNORE_MSGS Msgs to ignore (e.g. onroadEvents) --update-refs Updates reference logs using current commit - --upload-only Skips testing processes and uploads logs from previous test run ``` ## Forks diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index 9ba599bac9..d35474f373 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -9,7 +9,7 @@ from itertools import zip_longest import matplotlib.pyplot as plt import numpy as np -from tabulate import tabulate +from openpilot.common.utils import tabulate from openpilot.common.git import get_commit from openpilot.system.hardware import PC diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit deleted file mode 100644 index c109bf49f1..0000000000 --- a/selfdrive/test/process_replay/ref_commit +++ /dev/null @@ -1 +0,0 @@ -b259f6f8f099a9d82e4c65dd5deae2e4e293007b \ No newline at end of file diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 7bf8fe46d7..fbe300a7c9 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -9,14 +9,13 @@ from typing import Any from opendbc.car.car_helpers import interface_names from openpilot.common.git import get_commit -from openpilot.tools.lib.openpilotci import get_url, upload_file +from openpilot.tools.lib.openpilotci import get_url from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs, format_diff from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, PROC_REPLAY_DIR, FAKEDATA, replay_process, \ check_most_messages_valid from openpilot.tools.lib.filereader import FileReader from openpilot.tools.lib.logreader import LogReader, save_log - -IS_AZURE_TOKEN_DEFINED = os.getenv("AZURE_TOKEN") +from openpilot.tools.lib.url_file import URLFile source_segments = [ ("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.HYUNDAI_SONATA @@ -66,46 +65,17 @@ segments = [ # dashcamOnly makes don't need to be tested until a full port is done excluded_interfaces = ["mock", "body", "psa"] -BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/" +BASE_URL = "https://raw.githubusercontent.com/commaai/ci-artifacts/refs/heads/process-replay/" REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit") EXCLUDED_PROCS = {"modeld", "dmonitoringmodeld"} -def preserve_only_specified_files_from_ref_commit(*commits_to_keep): - """Keep only files in fakedata that contain any of the specified commit hashes.""" - removed = 0 - for f in os.listdir(FAKEDATA): - if not any(commit in f for commit in commits_to_keep): - os.remove(os.path.join(FAKEDATA, f)) - removed += 1 - if removed > 0: - print(f"Removed {removed} old files from {FAKEDATA}") - - -def handle_output_file(cur_log_fn, local): - """Handle the output file based on whether we're using remote or local storage.""" - assert os.path.exists(cur_log_fn), f"Cannot find log to upload: {cur_log_fn}" - - if local: - os.system(f"git add '{os.path.realpath(cur_log_fn)}'") - else: - upload_file(cur_log_fn, os.path.basename(cur_log_fn)) - os.remove(cur_log_fn) - - def run_test_process(data): segment, cfg, args, cur_log_fn, ref_log_path, lr_dat = data - res = None - if not args.upload_only: - lr = LogReader.from_bytes(lr_dat) - res, log_msgs = test_process(cfg, lr, segment, ref_log_path, cur_log_fn, args.ignore_fields, args.ignore_msgs) - # save logs so we can upload when updating refs - save_log(cur_log_fn, log_msgs) - - if args.update_refs or args.upload_only: - print(f'Processing: {os.path.basename(cur_log_fn)}') - handle_output_file(cur_log_fn, args.local) - + lr = LogReader.from_bytes(lr_dat) + res, log_msgs = test_process(cfg, lr, segment, ref_log_path, cur_log_fn, args.ignore_fields, args.ignore_msgs) + # save logs so we can update refs + save_log(cur_log_fn, log_msgs) return (segment, cfg.proc_name, res) @@ -144,27 +114,6 @@ def test_process(cfg, lr, segment, ref_log_path, new_log_path, ignore_fields=Non return str(e), log_msgs -def finalize_git_updates(cur_commit, ref_commit_fn): - """Finalize git updates and create commit.""" - try: - # Add all new files first - os.system(f"git add {os.path.realpath(ref_commit_fn)}") - os.system(f"git add {os.path.realpath(FAKEDATA)}/*.zst") - - # Clean up old files - keep only new ref files since they're becoming the reference - preserve_only_specified_files_from_ref_commit(cur_commit) - - # Add the deletions to git - os.system(f"git add -u {os.path.realpath(FAKEDATA)}") - - # Create the commit - commit_msg = f"test_processes: update ref logs to {cur_commit[:7]}" - os.system(f'git commit -m "{commit_msg}"') - print("Successfully committed reference log updates") - except Exception as e: - print(f"Failed to commit changes: {e}") - - if __name__ == "__main__": all_cars = {car for car, _ in segments} all_procs = {cfg.proc_name for cfg in CONFIGS if cfg.proc_name not in EXCLUDED_PROCS} @@ -186,10 +135,6 @@ if __name__ == "__main__": help="Msgs to ignore (e.g. carEvents)") parser.add_argument("--update-refs", action="store_true", help="Updates reference logs using current commit") - parser.add_argument("--upload-only", action="store_true", - help="Skips testing processes and uploads logs from previous test run") - parser.add_argument("--local", action="store_true", - help="Use local git/ storage instead of remote (Azure for Comma)") parser.add_argument("-j", "--jobs", type=int, default=max(cpu_count - 2, 1), help="Max amount of parallel jobs") args = parser.parse_args() @@ -199,33 +144,21 @@ if __name__ == "__main__": tested_cars = {c.upper() for c in tested_cars} full_test = (tested_procs == all_procs) and (tested_cars == all_cars) and all(len(x) == 0 for x in (args.ignore_fields, args.ignore_msgs)) - upload = args.update_refs or args.upload_only os.makedirs(os.path.dirname(FAKEDATA), exist_ok=True) - if upload: + if args.update_refs: assert full_test, "Need to run full test when updating refs" try: with open(REF_COMMIT_FN) as f: ref_commit = f.read().strip() except FileNotFoundError: - print("Couldn't find reference commit") - sys.exit(1) + ref_commit = URLFile(BASE_URL + "ref_commit", cache=False).read().decode().strip() cur_commit = get_commit() if not cur_commit: raise Exception("Couldn't get current commit") - # Could be set as default in args, but wanted to be more explicit on the flow. - if upload and not args.local and not IS_AZURE_TOKEN_DEFINED: - print("***** Warning: local/git run was used by default since AZURE_TOKEN was NOT found on the env variables! *****") - args.local = True - - # Clean up old files before starting - if upload and args.local: - print("***** Cleaning up old fakedata for local/git tracked refs *****") - preserve_only_specified_files_from_ref_commit(cur_commit, ref_commit) - print(f"***** testing against commit {ref_commit} *****") # check to make sure all car brands are tested @@ -235,12 +168,11 @@ if __name__ == "__main__": log_paths: defaultdict[str, dict[str, dict[str, str]]] = defaultdict(lambda: defaultdict(dict)) with concurrent.futures.ProcessPoolExecutor(max_workers=args.jobs) as pool: - if not args.upload_only: - download_segments = [seg for car, seg in segments if car in tested_cars] - log_data: dict[str, LogReader] = {} - p1 = pool.map(get_log_data, download_segments) - for segment, lr in tqdm(p1, desc="Getting Logs", total=len(download_segments)): - log_data[segment] = lr + download_segments = [seg for car, seg in segments if car in tested_cars] + log_data: dict[str, LogReader] = {} + p1 = pool.map(get_log_data, download_segments) + for segment, lr in tqdm(p1, desc="Getting Logs", total=len(download_segments)): + log_data[segment] = lr pool_args: Any = [] for car_brand, segment in segments: @@ -255,15 +187,15 @@ if __name__ == "__main__": if cfg.proc_name not in ('card', 'controlsd', 'lagd') and car_brand not in ('HYUNDAI', 'TOYOTA'): continue - cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst") + cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst".replace("|", "_")) if args.update_refs: # reference logs will not exist if routes were just regenerated - ref_log_path = get_url(*segment.rsplit("--", 1,), "rlog.zst") + route, seg_num = segment.rsplit("--", 1) + ref_log_path = get_url(route, seg_num, "rlog.zst") else: - ref_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{ref_commit}.zst") + ref_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{ref_commit}.zst".replace("|", "_")) ref_log_path = ref_log_fn if os.path.exists(ref_log_fn) else BASE_URL + os.path.basename(ref_log_fn) - dat = None if args.upload_only else log_data[segment] - pool_args.append((segment, cfg, args, cur_log_fn, ref_log_path, dat)) + pool_args.append((segment, cfg, args, cur_log_fn, ref_log_path, log_data[segment])) log_paths[segment][cfg.proc_name]['ref'] = ref_log_path log_paths[segment][cfg.proc_name]['new'] = cur_log_fn @@ -271,19 +203,16 @@ if __name__ == "__main__": results: Any = defaultdict(dict) p2 = pool.map(run_test_process, pool_args) for (segment, proc, result) in tqdm(p2, desc="Running Tests", total=len(pool_args)): - if not args.upload_only: - results[segment][proc] = result + results[segment][proc] = result diff_short, diff_long, failed = format_diff(results, log_paths, ref_commit) - if not upload: + if not args.update_refs: with open(os.path.join(PROC_REPLAY_DIR, "diff.txt"), "w") as f: f.write(diff_long) print(diff_short) if failed: print("TEST FAILED") - print("\n\nTo push the new reference logs for this commit run:") - print("./test_processes.py --upload-only") else: print("TEST SUCCEEDED") @@ -292,8 +221,4 @@ if __name__ == "__main__": f.write(cur_commit) print(f"\n\nUpdated reference logs for commit: {cur_commit}") - # Only do git operations if we're in local mode - if upload and args.local: - finalize_git_updates(cur_commit, REF_COMMIT_FN) - sys.exit(int(failed)) diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index f57751c067..008b8ebe7f 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -8,7 +8,7 @@ import time import numpy as np from collections import Counter, defaultdict from pathlib import Path -from tabulate import tabulate +from openpilot.common.utils import tabulate from cereal import log import cereal.messaging as messaging @@ -56,7 +56,7 @@ PROCS = { "selfdrive.ui.soundd": 3.0, "selfdrive.ui.feedback.feedbackd": 1.0, "selfdrive.monitoring.dmonitoringd": 4.0, - "system.proclogd": 3.0, + "system.proclogd": 7.0, "system.logmessaged": 1.0, "system.tombstoned": 0, "system.journald": 1.0, @@ -282,9 +282,12 @@ class TestOnroad: print("\n------------------------------------------------") print("--------------- Memory Usage -------------------") print("------------------------------------------------") + + from openpilot.selfdrive.debug.mem_usage import print_report + print_report(self.msgs['procLog'], self.msgs['deviceState']) + offset = int(SERVICE_LIST['deviceState'].frequency * LOG_OFFSET) mems = [m.deviceState.memoryUsagePercent for m in self.msgs['deviceState'][offset:]] - print("Overall memory usage: ", mems) print("MSGQ (/dev/shm/) usage: ", subprocess.check_output(["du", "-hs", "/dev/shm"]).split()[0].decode()) # check for big leaks. note that memory usage is diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index bdffea4cfe..0da5445ed2 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -92,22 +92,22 @@ class MiciHomeLayout(Widget): self._settings_txt = gui_app.texture("icons_mici/settings.png", 48, 48) self._experimental_txt = gui_app.texture("icons_mici/experimental_mode.png", 48, 48) - self._mic_txt = gui_app.texture("icons_mici/microphone.png", 48, 48) + self._mic_txt = gui_app.texture("icons_mici/microphone.png", 32, 46) self._net_type = NETWORK_TYPES.get(NetworkType.none) self._net_strength = 0 self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 50, 44) - self._wifi_none_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_none.png", 50, 44) - self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 50, 44) - self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 50, 44) - self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 50, 44) + self._wifi_none_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_none.png", 50, 37) + self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 50, 37) + self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 50, 37) + self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 50, 37) - self._cell_none_txt = gui_app.texture("icons_mici/settings/network/cell_strength_none.png", 55, 35) - self._cell_low_txt = gui_app.texture("icons_mici/settings/network/cell_strength_low.png", 55, 35) - self._cell_medium_txt = gui_app.texture("icons_mici/settings/network/cell_strength_medium.png", 55, 35) - self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 55, 35) - self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 55, 35) + self._cell_none_txt = gui_app.texture("icons_mici/settings/network/cell_strength_none.png", 54, 36) + self._cell_low_txt = gui_app.texture("icons_mici/settings/network/cell_strength_low.png", 54, 36) + self._cell_medium_txt = gui_app.texture("icons_mici/settings/network/cell_strength_medium.png", 54, 36) + self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 54, 36) + self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 54, 36) self._openpilot_label = MiciLabel("sunnypilot", font_size=90, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.AUDIOWIDE) self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index a83ebd1969..8f24d0b9b9 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -50,7 +50,7 @@ class MiciMainLayout(Widget): self._alerts_layout, self._home_layout, self._onroad_layout, - ], spacing=0, pad_start=0, pad_end=0) + ], spacing=0, pad_start=0, pad_end=0, scroll_indicator=False) self._scroller.set_reset_scroll_at_show(False) # Disable scrolling when onroad is interacting with bookmark diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index 8479ec5c54..9f9c7b83ab 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -127,9 +127,9 @@ class TrainingGuideDMTutorial(Widget): def __init__(self, continue_callback): super().__init__() - self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 48, 48)) + self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 28, 48)) self._back_button.set_click_callback(self._show_bad_face_page) - self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 48, 35)) + self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 42, 42)) # Wrap the continue callback to restore settings def wrapped_continue_callback(): diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index 8fc63e8963..b6145e042e 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -3,7 +3,7 @@ from collections.abc import Callable from openpilot.common.time_helpers import system_time_valid from openpilot.system.ui.widgets.scroller import Scroller -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle, BigParamControl +from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle, BigParamControl, BigCircleParamControl from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigInputDialog from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import NavWidget @@ -36,15 +36,15 @@ class DeveloperLayoutMici(NavWidget): return gui_app.set_modal_overlay(dlg) - txt_ssh = gui_app.texture("icons_mici/settings/developer/ssh.png", 77, 44) + txt_ssh = gui_app.texture("icons_mici/settings/developer/ssh.png", 56, 64) github_username = ui_state.params.get("GithubUsername") or "" self._ssh_keys_btn = BigButton("SSH keys", "Not set" if not github_username else github_username, icon=txt_ssh) self._ssh_keys_btn.set_click_callback(ssh_keys_callback) # adb, ssh, ssh keys, debug mode, joystick debug mode, longitudinal maneuver mode, ip address # ******** Main Scroller ******** - self._adb_toggle = BigParamControl("enable ADB", "AdbEnabled") - self._ssh_toggle = BigParamControl("enable SSH", "SshEnabled") + self._adb_toggle = BigCircleParamControl("icons_mici/adb_short.png", "AdbEnabled", icon_size=(82, 82), icon_offset=(0, 12)) + self._ssh_toggle = BigCircleParamControl("icons_mici/ssh_short.png", "SshEnabled", icon_size=(82, 82), icon_offset=(0, 12)) self._joystick_toggle = BigToggle("joystick debug mode", initial_state=ui_state.params.get_bool("JoystickDebugMode"), toggle_callback=self._on_joystick_debug_mode) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index 81f0e870ef..64ce80e231 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -1,6 +1,5 @@ import os import threading -import json import pyray as rl from enum import IntEnum from collections.abc import Callable @@ -11,7 +10,7 @@ from openpilot.common.time_helpers import system_time_valid from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton -from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigDialog, BigConfirmationDialogV2 +from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2 from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide @@ -119,7 +118,10 @@ class UpdaterState(IntEnum): class PairBigButton(BigButton): def __init__(self): - super().__init__("pair", "connect.comma.ai", "icons_mici/settings/comma_icon.png") + super().__init__("pair", "connect.comma.ai", "icons_mici/settings/comma_icon.png", icon_size=(33, 60)) + + def _get_label_font_size(self): + return 64 def _update_state(self): if ui_state.prime_state.is_paired(): @@ -153,8 +155,8 @@ UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond class UpdateOpenpilotBigButton(BigButton): def __init__(self): - self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 64) - self._txt_reboot_icon = gui_app.texture("icons_mici/settings/device/reboot.png", 64, 64) + self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75) + self._txt_reboot_icon = gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70) self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) super().__init__("update sunnypilot", "", self._txt_update_icon) @@ -222,7 +224,7 @@ class UpdateOpenpilotBigButton(BigButton): if self._waiting_for_updater_t is not None and rl.get_time() - self._waiting_for_updater_t > UPDATER_TIMEOUT: self.set_rotate_icon(False) - self.set_value("updater failed to respond") + self.set_value("updater failed\nto respond") self._state = UpdaterState.IDLE self._hide_value_t = rl.get_time() @@ -291,42 +293,26 @@ class DeviceLayoutMici(NavWidget): def uninstall_openpilot_callback(): ui_state.params.put_bool("DoUninstall", True) - reset_calibration_btn = BigButton("reset calibration", "", "icons_mici/settings/device/lkas.png") + reset_calibration_btn = BigButton("reset calibration", "", "icons_mici/settings/device/lkas.png", icon_size=(114, 60)) reset_calibration_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_calibration_callback, "reset")) uninstall_openpilot_btn = BigButton("uninstall sunnypilot", "", "icons_mici/settings/device/uninstall.png") uninstall_openpilot_btn.set_click_callback(lambda: _engaged_confirmation_callback(uninstall_openpilot_callback, "uninstall")) - reboot_btn = BigCircleButton("icons_mici/settings/device/reboot.png", red=False) + reboot_btn = BigCircleButton("icons_mici/settings/device/reboot.png", red=False, icon_size=(64, 70)) reboot_btn.set_click_callback(lambda: _engaged_confirmation_callback(reboot_callback, "reboot")) - self._power_off_btn = BigCircleButton("icons_mici/settings/device/power.png", red=True) + self._power_off_btn = BigCircleButton("icons_mici/settings/device/power.png", red=True, icon_size=(64, 66)) self._power_off_btn.set_click_callback(lambda: _engaged_confirmation_callback(power_off_callback, "power off")) - self._load_languages() - - def language_callback(): - def selected_language_callback(): - selected_language = dlg.get_selected_option() - ui_state.params.put("LanguageSetting", self._languages[selected_language]) - - current_language_name = ui_state.params.get("LanguageSetting") - current_language = next(name for name, lang in self._languages.items() if lang == current_language_name) - - dlg = BigMultiOptionDialog(list(self._languages), default=current_language, right_btn_callback=selected_language_callback) - gui_app.set_modal_overlay(dlg) - - # lang_button = BigButton("change language", "", "icons_mici/settings/device/language.png") - # lang_button.set_click_callback(language_callback) - regulatory_btn = BigButton("regulatory info", "", "icons_mici/settings/device/info.png") regulatory_btn.set_click_callback(self._on_regulatory) - driver_cam_btn = BigButton("driver camera preview", "", "icons_mici/settings/device/cameras.png") + driver_cam_btn = BigButton("driver\ncamera preview", "", "icons_mici/settings/device/cameras.png") driver_cam_btn.set_click_callback(self._show_driver_camera) driver_cam_btn.set_enabled(lambda: ui_state.is_offroad()) - review_training_guide_btn = BigButton("review training guide", "", "icons_mici/settings/device/info.png") + review_training_guide_btn = BigButton("review\ntraining guide", "", "icons_mici/settings/device/info.png") review_training_guide_btn.set_click_callback(self._on_review_training_guide) review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad()) @@ -353,7 +339,7 @@ class DeviceLayoutMici(NavWidget): def _on_regulatory(self): if not self._fcc_dialog: self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html")) - gui_app.set_modal_overlay(self._fcc_dialog, callback=setattr(self, '_fcc_dialog', None)) + gui_app.set_modal_overlay(self._fcc_dialog) def _offroad_transition(self): self._power_off_btn.set_visible(ui_state.is_offroad()) @@ -371,10 +357,6 @@ class DeviceLayoutMici(NavWidget): self._training_guide = TrainingGuide(completed_callback=completed_callback) gui_app.set_modal_overlay(self._training_guide, callback=lambda result: setattr(self, '_training_guide', None)) - def _load_languages(self): - with open(os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json")) as f: - self._languages = json.load(f) - def show_event(self): super().show_event() self._scroller.show_event() diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 1faf49311a..fb1d56a1f6 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -3,8 +3,8 @@ from enum import IntEnum from collections.abc import Callable from openpilot.system.ui.widgets.scroller import Scroller -from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle, BigParamControl +from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon, normalize_ssid +from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.lib.prime_state import PrimeType @@ -33,8 +33,6 @@ class NetworkLayoutMici(NavWidget): networks_updated=self._on_network_updated, ) - _tethering_icon = "icons_mici/settings/network/tethering.png" - # ******** Tethering ******** def tethering_toggle_callback(checked: bool): self._tethering_toggle_btn.set_enabled(False) @@ -53,13 +51,10 @@ class NetworkLayoutMici(NavWidget): confirm_callback=tethering_password_callback) gui_app.set_modal_overlay(dlg) - txt_tethering = gui_app.texture(_tethering_icon, 64, 53) + txt_tethering = gui_app.texture("icons_mici/settings/network/tethering.png", 64, 54) self._tethering_password_btn = BigButton("tethering password", "", txt_tethering) self._tethering_password_btn.set_click_callback(tethering_password_clicked) - # ******** IP Address ******** - self._ip_address_btn = BigButton("IP Address", "Not connected") - # ******** Network Metered ******** def network_metered_callback(value: str): self._network_metered_btn.set_enabled(False) @@ -75,8 +70,13 @@ class NetworkLayoutMici(NavWidget): self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback) self._network_metered_btn.set_enabled(False) - wifi_button = BigButton("wi-fi") - wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI)) + self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 64, 56) + self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 64, 47) + self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 64, 47) + self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47) + + self._wifi_button = BigButton("wi-fi", "not connected", self._wifi_slash_txt, scroll=True) + self._wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI)) # ******** Advanced settings ******** # ******** Roaming toggle ******** @@ -91,7 +91,7 @@ class NetworkLayoutMici(NavWidget): # Main scroller ---------------------------------- self._scroller = Scroller([ - wifi_button, + self._wifi_button, self._network_metered_btn, self._tethering_toggle_btn, self._tethering_password_btn, @@ -100,7 +100,6 @@ class NetworkLayoutMici(NavWidget): self._apn_btn, self._cellular_metered_btn, # */ - self._ip_address_btn, ], snap_items=False) # Set initial config @@ -159,8 +158,22 @@ class NetworkLayoutMici(NavWidget): self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address)) self._tethering_toggle_btn.set_checked(tethering_active) - # Update IP address - self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or "Not connected") + # Update wi-fi button with ssid and ip address + # TODO: make sure we handle hidden ssids + connected_network = next((network for network in networks if network.is_connected), None) + self._wifi_button.set_text(normalize_ssid(connected_network.ssid) if connected_network is not None else "wi-fi") + self._wifi_button.set_value(self._wifi_manager.ipv4_address or "not connected") + if connected_network is not None: + strength = WifiIcon.get_strength_icon_idx(connected_network.strength) + if strength == 2: + strength_icon = self._wifi_full_txt + elif strength == 1: + strength_icon = self._wifi_medium_txt + else: + strength_icon = self._wifi_low_txt + self._wifi_button.set_icon(strength_icon) + else: + self._wifi_button.set_icon(self._wifi_slash_txt) # Update network metered self._network_metered_btn.set_value( diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 565fef5af3..db68632c0e 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -34,12 +34,12 @@ class LoadingAnimation(Widget): class WifiIcon(Widget): def __init__(self): super().__init__() - self.set_rect(rl.Rectangle(0, 0, 89, 64)) + self.set_rect(rl.Rectangle(0, 0, 86, 64)) - self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 89, 64) - self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 89, 64) - self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 89, 64) - self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 23, 32) + self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 86, 64) + self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 86, 64) + self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 86, 64) + self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 22, 32) self._network: Network | None = None self._scale = 1.0 @@ -50,12 +50,16 @@ class WifiIcon(Widget): def set_scale(self, scale: float): self._scale = scale + @staticmethod + def get_strength_icon_idx(strength: int) -> int: + return round(strength / 100 * 2) + def _render(self, _): if self._network is None: return # Determine which wifi strength icon to use - strength = round(self._network.strength / 100 * 2) + strength = self.get_strength_icon_idx(self._network.strength) if strength == 2: strength_icon = self._wifi_full_txt elif strength == 1: @@ -169,7 +173,7 @@ class ForgetButton(Widget): self._bg_txt = gui_app.texture("icons_mici/settings/network/new/forget_button.png", 100, 100) self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/forget_button_pressed.png", 100, 100) - self._trash_txt = gui_app.texture("icons_mici/settings/network/new/trash.png", 32, 36) + self._trash_txt = gui_app.texture("icons_mici/settings/network/new/trash.png", 35, 42) self.set_rect(rl.Rectangle(0, 0, 100 + self.HORIZONTAL_MARGIN * 2, 100)) def _handle_mouse_release(self, mouse_pos: MousePos): @@ -314,7 +318,7 @@ class WifiUIMici(BigMultiOptionDialog): INACTIVITY_TIMEOUT = 1 def __init__(self, wifi_manager: WifiManager, back_callback: Callable): - super().__init__([], None, None, right_btn_callback=None) + super().__init__([], None) # Set up back navigation self.set_back_callback(back_callback) diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index a452777748..a6f59a0389 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -30,22 +30,27 @@ class PanelInfo: instance: Widget +class SettingsBigButton(BigButton): + def _get_label_font_size(self): + return 64 + + class SettingsLayout(NavWidget): def __init__(self): super().__init__() self._params = Params() self._current_panel = None # PanelType.DEVICE - toggles_btn = BigButton("toggles", "", "icons_mici/settings/toggles_icon.png") + toggles_btn = SettingsBigButton("toggles", "", "icons_mici/settings.png") toggles_btn.set_click_callback(lambda: self._set_current_panel(PanelType.TOGGLES)) - network_btn = BigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png") + network_btn = SettingsBigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56)) network_btn.set_click_callback(lambda: self._set_current_panel(PanelType.NETWORK)) - device_btn = BigButton("device", "", "icons_mici/settings/device_icon.png") + device_btn = SettingsBigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60)) device_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVICE)) - developer_btn = BigButton("developer", "", "icons_mici/settings/developer_icon.png") + developer_btn = SettingsBigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60)) developer_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVELOPER)) - firehose_btn = BigButton("firehose", "", "icons_mici/settings/comma_icon.png") + firehose_btn = SettingsBigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62)) firehose_btn.set_click_callback(lambda: self._set_current_panel(PanelType.FIREHOSE)) self._scroller = Scroller([ diff --git a/selfdrive/ui/mici/onroad/alert_renderer.py b/selfdrive/ui/mici/onroad/alert_renderer.py index 5c93e5e423..b7a336a35a 100644 --- a/selfdrive/ui/mici/onroad/alert_renderer.py +++ b/selfdrive/ui/mici/onroad/alert_renderer.py @@ -111,10 +111,10 @@ class AlertRenderer(Widget): self._load_icons() def _load_icons(self): - self._txt_turn_signal_left = gui_app.texture('icons_mici/onroad/turn_signal_left.png', 100, 91) - self._txt_turn_signal_right = gui_app.texture('icons_mici/onroad/turn_signal_right.png', 100, 91) - self._txt_blind_spot_left = gui_app.texture('icons_mici/onroad/blind_spot_left.png', 108, 128) - self._txt_blind_spot_right = gui_app.texture('icons_mici/onroad/blind_spot_right.png', 108, 128) + self._txt_turn_signal_left = gui_app.texture('icons_mici/onroad/turn_signal_left.png', 104, 96) + self._txt_turn_signal_right = gui_app.texture('icons_mici/onroad/turn_signal_right.png', 104, 96) + self._txt_blind_spot_left = gui_app.texture('icons_mici/onroad/blind_spot_left.png', 134, 150) + self._txt_blind_spot_right = gui_app.texture('icons_mici/onroad/blind_spot_right.png', 134, 150) def get_alert(self, sm: messaging.SubMaster) -> Alert | None: """Generate the current alert based on selfdrive state.""" diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index 316052fdc1..f8aee7969f 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -14,7 +14,7 @@ from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets import Widget -from openpilot.common.filter_simple import BounceFilter +from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame from openpilot.common.transformations.orientation import rot_from_euler from enum import IntEnum @@ -50,6 +50,8 @@ class BookmarkIcon(Widget): super().__init__() self._bookmark_callback = bookmark_callback self._icon = gui_app.texture("icons_mici/onroad/bookmark.png", 180, 180) + self._icon_fill = gui_app.texture("icons_mici/onroad/bookmark_fill.png", 180, 180) + self._active_icon = self._icon self._offset_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps) # State @@ -88,6 +90,7 @@ class BookmarkIcon(Widget): if self._offset_filter.x < 1e-3: self._interacting = False + self._active_icon = self._icon def _handle_mouse_event(self, mouse_event: MouseEvent): if not ui_state.started: @@ -100,6 +103,7 @@ class BookmarkIcon(Widget): self._is_swiping = True self._is_swiping_left = False self._state = BookmarkState.DRAGGING + self._active_icon = self._icon elif mouse_event.left_down and self._is_swiping: self._swipe_current_x = mouse_event.pos.x @@ -116,6 +120,7 @@ class BookmarkIcon(Widget): if swipe_distance > self.PEEK_THRESHOLD: self._state = BookmarkState.TRIGGERED self._triggered_time = rl.get_time() + self._active_icon = self._icon_fill self._bookmark_callback() else: # Otherwise, transition back to hidden @@ -129,8 +134,8 @@ class BookmarkIcon(Widget): """Render the bookmark icon.""" if self._offset_filter.x > 0: icon_x = self.rect.x + self.rect.width - round(self._offset_filter.x) - icon_y = self.rect.y + (self.rect.height - self._icon.height) / 2 # Vertically centered - rl.draw_texture(self._icon, int(icon_x), int(icon_y), rl.WHITE) + icon_y = self.rect.y + (self.rect.height - self._active_icon.height) / 2 # Vertically centered + rl.draw_texture(self._active_icon, int(icon_x), int(icon_y), rl.WHITE) class AugmentedRoadView(CameraView): @@ -164,6 +169,7 @@ class AugmentedRoadView(CameraView): alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png") + self._fade_alpha_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) # debug self._pm = messaging.PubMaster(['uiDebug']) @@ -216,8 +222,11 @@ class AugmentedRoadView(CameraView): # Draw all UI overlays self._model_renderer.render(self._content_rect) - # Fade out bottom of overlays for looks - rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0, rl.WHITE) + # Fade out bottom of overlays for looks (only when engaged) + fade_alpha = self._fade_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED) + if fade_alpha > 1e-2: + rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0, + rl.Color(255, 255, 255, int(255 * fade_alpha))) alert_to_render, not_animating_out = self._alert_renderer.will_render() diff --git a/selfdrive/ui/mici/onroad/hud_renderer.py b/selfdrive/ui/mici/onroad/hud_renderer.py index 44ce12437a..659d13fad7 100644 --- a/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/selfdrive/ui/mici/onroad/hud_renderer.py @@ -49,8 +49,8 @@ class TurnIntent(Widget): self._turn_intent_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) self._turn_intent_rotation_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) - self._txt_turn_intent_left: rl.Texture = gui_app.texture('icons_mici/turn_intent_left.png', 50, 19) - self._txt_turn_intent_right: rl.Texture = gui_app.texture('icons_mici/turn_intent_right.png', 50, 19) + self._txt_turn_intent_left: rl.Texture = gui_app.texture('icons_mici/turn_intent_left.png', 50, 20) + self._txt_turn_intent_right: rl.Texture = gui_app.texture('icons_mici/turn_intent_right.png', 50, 20) def _render(self, _): if self._turn_intent_alpha_filter.x > 1e-2: diff --git a/selfdrive/ui/mici/onroad/model_renderer.py b/selfdrive/ui/mici/onroad/model_renderer.py index 0908de0bf4..ca051c6d94 100644 --- a/selfdrive/ui/mici/onroad/model_renderer.py +++ b/selfdrive/ui/mici/onroad/model_renderer.py @@ -12,7 +12,7 @@ from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient from openpilot.system.ui.widgets import Widget -from openpilot.selfdrive.ui.sunnypilot.mici.onroad.model_renderer import LANE_LINE_COLORS_SP +from openpilot.selfdrive.ui.sunnypilot.mici.onroad.model_renderer import LANE_LINE_COLORS_SP, ModelRendererSP CLIP_MARGIN = 500 MIN_DRAW_DISTANCE = 10.0 @@ -51,9 +51,10 @@ class LeadVehicle: fill_alpha: int = 0 -class ModelRenderer(Widget): +class ModelRenderer(Widget, ModelRendererSP): def __init__(self): - super().__init__() + Widget.__init__(self) + ModelRendererSP.__init__(self) self._longitudinal_control = False self._experimental_mode = False self._blend_filter = FirstOrderFilter(1.0, 0.25, 1 / gui_app.target_fps) @@ -340,6 +341,10 @@ class ModelRenderer(Widget): allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control self._blend_filter.update(int(allow_throttle)) + if ui_state.rainbow_path: + self.rainbow_path.draw_rainbow_path(self._rect, self._path) + return + if self._experimental_mode: # Draw with acceleration coloring if ui_state.status == UIStatus.DISENGAGED: diff --git a/selfdrive/ui/mici/onroad/torque_bar.py b/selfdrive/ui/mici/onroad/torque_bar.py index c8485a3101..c1de694633 100644 --- a/selfdrive/ui/mici/onroad/torque_bar.py +++ b/selfdrive/ui/mici/onroad/torque_bar.py @@ -146,9 +146,11 @@ def arc_bar_pts(cx: float, cy: float, class TorqueBar(Widget): - def __init__(self, demo: bool = False): + def __init__(self, demo: bool = False, scale: float = 1.0, always: bool = False): super().__init__() self._demo = demo + self._scale = scale + self._always = always self._torque_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) self._torque_line_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) @@ -180,8 +182,8 @@ class TorqueBar(Widget): def _render(self, rect: rl.Rectangle) -> None: # adjust y pos with torque - torque_line_offset = np.interp(abs(self._torque_filter.x), [0.5, 1], [22, 26]) - torque_line_height = np.interp(abs(self._torque_filter.x), [0.5, 1], [14, 56]) + torque_line_offset = np.interp(abs(self._torque_filter.x), [0.5, 1], [22 * self._scale, 26 * self._scale]) + torque_line_height = np.interp(abs(self._torque_filter.x), [0.5, 1], [14 * self._scale, 56 * self._scale]) # animate alpha and angle span if not self._demo: @@ -195,7 +197,7 @@ class TorqueBar(Widget): torque_line_bg_color = rl.Color(255, 255, 255, int(255 * 0.15 * self._torque_line_alpha_filter.x)) # draw curved line polygon torque bar - torque_line_radius = 1200 + torque_line_radius = 1200 * self._scale top_angle = -90 torque_bg_angle_span = self._torque_line_alpha_filter.x * TORQUE_ANGLE_SPAN torque_start_angle = top_angle - torque_bg_angle_span / 2 @@ -207,13 +209,13 @@ class TorqueBar(Widget): cy = rect.y + rect.height + torque_line_radius - torque_line_offset # draw bg torque indicator line - bg_pts = arc_bar_pts(cx, cy, mid_r, torque_line_height, torque_start_angle, torque_end_angle) + bg_pts = arc_bar_pts(cx, cy, mid_r, torque_line_height, torque_start_angle, torque_end_angle, cap_radius=7 * self._scale) draw_polygon(rect, bg_pts, color=torque_line_bg_color) # draw torque indicator line a0s = top_angle a1s = a0s + torque_bg_angle_span / 2 * self._torque_filter.x - sl_pts = arc_bar_pts(cx, cy, mid_r, torque_line_height, a0s, a1s) + sl_pts = arc_bar_pts(cx, cy, mid_r, torque_line_height, a0s, a1s, cap_radius=7 * self._scale) # draw beautiful gradient from center to 65% of the bg torque bar width start_grad_pt = cx / rect.width @@ -252,5 +254,5 @@ class TorqueBar(Widget): # draw center torque bar dot if abs(self._torque_filter.x) < 0.5: dot_y = self._rect.y + self._rect.height - torque_line_offset - torque_line_height / 2 - rl.draw_circle(int(cx), int(dot_y), 10 // 2, + rl.draw_circle(int(cx), int(dot_y), (10 // 2 * self._scale), rl.Color(182, 182, 182, int(255 * 0.9 * self._torque_line_alpha_filter.x))) diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index 82310577b0..29844ccda1 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -3,9 +3,8 @@ from typing import Union from enum import Enum from collections.abc import Callable from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import MiciLabel +from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.scroller import DO_ZOOM -from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.common.filter_simple import BounceFilter @@ -16,8 +15,9 @@ except ImportError: SCROLLING_SPEED_PX_S = 50 COMPLICATION_SIZE = 36 -LABEL_COLOR = rl.WHITE +LABEL_COLOR = rl.Color(255, 255, 255, int(255 * 0.9)) LABEL_HORIZONTAL_PADDING = 40 +LABEL_VERTICAL_PADDING = 23 # visually matches 30 in figma COMPLICATION_GREY = rl.Color(0xAA, 0xAA, 0xAA, 255) PRESSED_SCALE = 1.15 if DO_ZOOM else 1.07 @@ -29,9 +29,10 @@ class ScrollState(Enum): class BigCircleButton(Widget): - def __init__(self, icon: str, red: bool = False): + def __init__(self, icon: str, red: bool = False, icon_size: tuple[int, int] = (64, 53), icon_offset: tuple[int, int] = (0, 0)): super().__init__() self._red = red + self._icon_offset = icon_offset # State self.set_rect(rl.Rectangle(0, 0, 180, 180)) @@ -39,7 +40,7 @@ class BigCircleButton(Widget): self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) # Icons - self._txt_icon = gui_app.texture(icon, 64, 53) + self._txt_icon = gui_app.texture(icon, *icon_size) self._txt_btn_disabled_bg = gui_app.texture("icons_mici/buttons/button_circle_disabled.png", 180, 180) self._txt_btn_bg = gui_app.texture("icons_mici/buttons/button_circle.png", 180, 180) @@ -51,6 +52,12 @@ class BigCircleButton(Widget): def set_enable_pressed_state(self, pressed: bool): self._press_state_enabled = pressed + def _draw_content(self, btn_y: float): + # draw icon + icon_color = rl.WHITE if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35)) + rl.draw_texture_ex(self._txt_icon, (self._rect.x + (self._rect.width - self._txt_icon.width) / 2 + self._icon_offset[0], + btn_y + (self._rect.height - self._txt_icon.height) / 2 + self._icon_offset[1]), 0, 1.0, icon_color) + def _render(self, _): # draw background txt_bg = self._txt_btn_bg if not self._red else self._txt_btn_red_bg @@ -64,15 +71,12 @@ class BigCircleButton(Widget): btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2 rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE) - # draw icon - icon_color = rl.WHITE if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35)) - rl.draw_texture(self._txt_icon, int(self._rect.x + (self._rect.width - self._txt_icon.width) / 2), - int(self._rect.y + (self._rect.height - self._txt_icon.height) / 2), icon_color) + self._draw_content(btn_y) class BigCircleToggle(BigCircleButton): - def __init__(self, icon: str, toggle_callback: Callable | None = None): - super().__init__(icon, False) + def __init__(self, icon: str, toggle_callback: Callable | None = None, icon_size: tuple[int, int] = (64, 53), icon_offset: tuple[int, int] = (0, 0)): + super().__init__(icon, False, icon_size=icon_size, icon_offset=icon_offset) self._toggle_callback = toggle_callback # State @@ -80,7 +84,7 @@ class BigCircleToggle(BigCircleButton): # Icons self._txt_toggle_enabled = gui_app.texture("icons_mici/buttons/toggle_dot_enabled.png", 66, 66) - self._txt_toggle_disabled = gui_app.texture("icons_mici/buttons/toggle_dot_disabled.png", 70, 70) # TODO: why discrepancy? + self._txt_toggle_disabled = gui_app.texture("icons_mici/buttons/toggle_dot_disabled.png", 66, 66) def set_checked(self, checked: bool): self._checked = checked @@ -92,49 +96,43 @@ class BigCircleToggle(BigCircleButton): if self._toggle_callback: self._toggle_callback(self._checked) - def _render(self, _): - super()._render(_) + def _draw_content(self, btn_y: float): + super()._draw_content(btn_y) # draw status icon - rl.draw_texture(self._txt_toggle_enabled if self._checked else self._txt_toggle_disabled, - int(self._rect.x + (self._rect.width - self._txt_toggle_enabled.width) / 2), - int(self._rect.y + 5), rl.WHITE) + rl.draw_texture_ex(self._txt_toggle_enabled if self._checked else self._txt_toggle_disabled, + (self._rect.x + (self._rect.width - self._txt_toggle_enabled.width) / 2, btn_y + 5), + 0, 1.0, rl.WHITE) class BigButton(Widget): """A lightweight stand-in for the Qt BigButton, drawn & updated each frame.""" - def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = ""): + def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", icon_size: tuple[int, int] = (64, 64), + scroll: bool = False): super().__init__() self.set_rect(rl.Rectangle(0, 0, 402, 180)) self.text = text self.value = value + self._icon_size = icon_size + self._scroll = scroll self.set_icon(icon) self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) self._rotate_icon_t: float | None = None - self._label_font = gui_app.font(FontWeight.DISPLAY) - self._value_font = gui_app.font(FontWeight.ROMAN) - - self._label = MiciLabel(text, font_size=self._get_label_font_size(), width=int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2), - font_weight=FontWeight.DISPLAY, color=LABEL_COLOR, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, wrap_text=True) - self._sub_label = MiciLabel(value, font_size=COMPLICATION_SIZE, width=int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2), - font_weight=FontWeight.ROMAN, color=COMPLICATION_GREY, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, wrap_text=True) + self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD, + text_color=LABEL_COLOR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, scroll=scroll, + line_height=0.9) + self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN, + text_color=COMPLICATION_GREY, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) + self._update_label_layout() self._load_images() - # internal state - self._scroll_offset = 0 # in pixels - self._needs_scroll = measure_text_cached(self._label_font, text, self._get_label_font_size()).x + 25 > self._rect.width - self._scroll_timer = 0 - self._scroll_state = ScrollState.PRE_SCROLL - def set_icon(self, icon: Union[str, rl.Texture]): - self._txt_icon = gui_app.texture(icon, 64, 64) if isinstance(icon, str) and len(icon) else icon + self._txt_icon = gui_app.texture(icon, *self._icon_size) if isinstance(icon, str) and len(icon) else icon def set_rotate_icon(self, rotate: bool): if rotate and self._rotate_icon_t is not None: @@ -147,28 +145,33 @@ class BigButton(Widget): self._txt_disabled_bg = gui_app.texture("icons_mici/buttons/button_rectangle_disabled.png", 402, 180) self._txt_hover_bg = gui_app.texture("icons_mici/buttons/button_rectangle_hover.png", 402, 180) + def _width_hint(self) -> int: + # Single line if scrolling, so hide behind icon if exists + icon_size = self._icon_size[0] if self._txt_icon and self._scroll and self.value else 0 + return int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - icon_size) + def _get_label_font_size(self): - if len(self.text) < 12: - font_size = 64 - elif len(self.text) < 17: - font_size = 48 - elif len(self.text) < 20: - font_size = 42 + if len(self.text) <= 18: + return 48 else: - font_size = 36 + return 42 + def _update_label_layout(self): + self._label.set_font_size(self._get_label_font_size()) if self.value: - font_size -= 20 - - return font_size + self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) + else: + self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) def set_text(self, text: str): self.text = text self._label.set_text(text) + self._update_label_layout() def set_value(self, value: str): self.value = value self._sub_label.set_text(value) + self._update_label_layout() def get_value(self) -> str: return self.value @@ -176,37 +179,35 @@ class BigButton(Widget): def get_text(self): return self.text - def _update_state(self): - # hold on text for a bit, scroll, hold again, reset - if self._needs_scroll: - """`dt` should be seconds since last frame (rl.get_frame_time()).""" - # TODO: this comment is generated by GPT, prob wrong and misused - dt = rl.get_frame_time() + def _draw_content(self, btn_y: float): + # LABEL ------------------------------------------------------------------ + label_x = self._rect.x + LABEL_HORIZONTAL_PADDING - self._scroll_timer += dt - if self._scroll_state == ScrollState.PRE_SCROLL: - if self._scroll_timer < 0.5: - return - self._scroll_state = ScrollState.SCROLLING - self._scroll_timer = 0 + label_color = LABEL_COLOR if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35)) + self._label.set_color(label_color) + label_rect = rl.Rectangle(label_x, btn_y + LABEL_VERTICAL_PADDING, self._width_hint(), + self._rect.height - LABEL_VERTICAL_PADDING * 2) + self._label.render(label_rect) - elif self._scroll_state == ScrollState.SCROLLING: - self._scroll_offset -= SCROLLING_SPEED_PX_S * dt - # reset when text has completely left the button + 50 px gap - # TODO: use global constant for 30+30 px gap - # TODO: add std Widget padding option integrated into the self._rect - full_len = measure_text_cached(self._label_font, self.text, self._get_label_font_size()).x + 30 + 30 - if self._scroll_offset < (self._rect.width - full_len): - self._scroll_state = ScrollState.POST_SCROLL - self._scroll_timer = 0 + if self.value: + label_y = btn_y + self._rect.height - LABEL_VERTICAL_PADDING + sub_label_height = self._sub_label.get_content_height(self._width_hint()) + sub_label_rect = rl.Rectangle(label_x, label_y - sub_label_height, self._width_hint(), sub_label_height) + self._sub_label.render(sub_label_rect) - elif self._scroll_state == ScrollState.POST_SCROLL: - # wait for a bit before starting to scroll again - if self._scroll_timer < 0.75: - return - self._scroll_state = ScrollState.PRE_SCROLL - self._scroll_timer = 0 - self._scroll_offset = 0 + # ICON ------------------------------------------------------------------- + if self._txt_icon: + rotation = 0 + if self._rotate_icon_t is not None: + rotation = (rl.get_time() - self._rotate_icon_t) * 180 + + # draw top right with 30px padding + x = self._rect.x + self._rect.width - 30 - self._txt_icon.width / 2 + y = btn_y + 30 + self._txt_icon.height / 2 + source_rec = rl.Rectangle(0, 0, self._txt_icon.width, self._txt_icon.height) + dest_rec = rl.Rectangle(x, y, self._txt_icon.width, self._txt_icon.height) + origin = rl.Vector2(self._txt_icon.width / 2, self._txt_icon.height / 2) + rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.WHITE) def _render(self, _): # draw _txt_default_bg @@ -221,33 +222,7 @@ class BigButton(Widget): btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2 rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE) - # LABEL ------------------------------------------------------------------ - lx = self._rect.x + LABEL_HORIZONTAL_PADDING - ly = btn_y + self._rect.height - 33 # - 40# - self._get_label_font_size() / 2 - - if self.value: - self._sub_label.set_position(lx, ly) - ly -= self._sub_label.font_size + 9 - self._sub_label.render() - - label_color = LABEL_COLOR if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35)) - self._label.set_color(label_color) - self._label.set_position(lx, ly) - self._label.render() - - # ICON ------------------------------------------------------------------- - if self._txt_icon: - rotation = 0 - if self._rotate_icon_t is not None: - rotation = (rl.get_time() - self._rotate_icon_t) * 180 - - # drop top right with 30px padding - x = self._rect.x + self._rect.width - 30 - self._txt_icon.width / 2 - y = self._rect.y + 30 + self._txt_icon.height / 2 - source_rec = rl.Rectangle(0, 0, self._txt_icon.width, self._txt_icon.height) - dest_rec = rl.Rectangle(int(x), int(y), self._txt_icon.width, self._txt_icon.height) - origin = rl.Vector2(self._txt_icon.width / 2, self._txt_icon.height / 2) - rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.WHITE) + self._draw_content(btn_y) class BigToggle(BigButton): @@ -256,8 +231,6 @@ class BigToggle(BigButton): self._checked = initial_state self._toggle_callback = toggle_callback - self._label.set_font_size(48) - def _load_images(self): super()._load_images() self._txt_enabled_toggle = gui_app.texture("icons_mici/buttons/toggle_pill_enabled.png", 84, 66) @@ -275,15 +248,15 @@ class BigToggle(BigButton): def _draw_pill(self, x: float, y: float, checked: bool): # draw toggle icon top right if checked: - rl.draw_texture(self._txt_enabled_toggle, int(x), int(y), rl.WHITE) + rl.draw_texture_ex(self._txt_enabled_toggle, (x, y), 0, 1.0, rl.WHITE) else: - rl.draw_texture(self._txt_disabled_toggle, int(x), int(y), rl.WHITE) + rl.draw_texture_ex(self._txt_disabled_toggle, (x, y), 0, 1.0, rl.WHITE) - def _render(self, _): - super()._render(_) + def _draw_content(self, btn_y: float): + super()._draw_content(btn_y) x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width - y = self._rect.y + y = btn_y self._draw_pill(x, y, self._checked) @@ -295,15 +268,10 @@ class BigMultiToggle(BigToggle): self._options = options self._select_callback = select_callback - self._label.set_width(int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width)) - # TODO: why isn't this automatic? - self._label.set_font_size(self._get_label_font_size()) - self.set_value(self._options[0]) - def _get_label_font_size(self): - font_size = super()._get_label_font_size() - return font_size - 6 + def _width_hint(self) -> int: + return int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width) def _handle_mouse_release(self, mouse_pos: MousePos): super()._handle_mouse_release(mouse_pos) @@ -313,13 +281,14 @@ class BigMultiToggle(BigToggle): if self._select_callback: self._select_callback(self.value) - def _render(self, _): - BigButton._render(self, _) + def _draw_content(self, btn_y: float): + # don't draw pill from BigToggle + BigButton._draw_content(self, btn_y) checked_idx = self._options.index(self.value) x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width - y = self._rect.y + y = btn_y for i in range(len(self._options)): self._draw_pill(x, y, checked_idx == i) @@ -361,8 +330,9 @@ class BigParamControl(BigToggle): # TODO: param control base class class BigCircleParamControl(BigCircleToggle): - def __init__(self, icon: str, param: str, toggle_callback: Callable | None = None): - super().__init__(icon, toggle_callback) + def __init__(self, icon: str, param: str, toggle_callback: Callable | None = None, icon_size: tuple[int, int] = (64, 53), + icon_offset: tuple[int, int] = (0, 0)): + super().__init__(icon, toggle_callback, icon_size=icon_size, icon_offset=icon_offset) self._param = param self.params = Params() self.set_checked(self.params.get_bool(self._param, False)) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index abd558aa8d..b88ac20494 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -14,7 +14,6 @@ from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider from openpilot.common.filter_simple import FirstOrderFilter from openpilot.selfdrive.ui.mici.widgets.button import BigButton -from openpilot.selfdrive.ui.mici.widgets.side_button import SideButton DEBUG = False @@ -22,32 +21,17 @@ PADDING = 20 class BigDialogBase(NavWidget, abc.ABC): - def __init__(self, right_btn: str | None = None, right_btn_callback: Callable | None = None): + def __init__(self): super().__init__() self._ret = DialogResult.NO_ACTION self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) self.set_back_callback(lambda: setattr(self, '_ret', DialogResult.CANCEL)) - self._right_btn = None - if right_btn: - def right_btn_callback_wrapper(): - gui_app.set_modal_overlay(None) - if right_btn_callback: - right_btn_callback() - - self._right_btn = SideButton(right_btn) - self._right_btn.set_click_callback(right_btn_callback_wrapper) - # move to right side - self._right_btn._rect.x = self._rect.x + self._rect.width - self._right_btn._rect.width - def _render(self, _) -> DialogResult: """ Allows `gui_app.set_modal_overlay(BigDialog(...))`. The overlay runner keeps calling until result != NO_ACTION. """ - if self._right_btn: - self._right_btn.set_position(self._right_btn._rect.x, self._rect.y) - self._right_btn.render() return self._ret @@ -55,10 +39,8 @@ class BigDialogBase(NavWidget, abc.ABC): class BigDialog(BigDialogBase): def __init__(self, title: str, - description: str, - right_btn: str | None = None, - right_btn_callback: Callable | None = None): - super().__init__(right_btn, right_btn_callback) + description: str): + super().__init__() self._title = title self._description = description @@ -70,8 +52,6 @@ class BigDialog(BigDialogBase): # TODO: coming up with these numbers manually is a pain and not scalable # TODO: no clue what any of these numbers mean. VBox and HBox would remove all of this shite max_width = self._rect.width - PADDING * 2 - if self._right_btn: - max_width -= self._right_btn._rect.width title_wrapped = '\n'.join(wrap_text(gui_app.font(FontWeight.BOLD), self._title, 50, int(max_width))) title_size = measure_text_cached(gui_app.font(FontWeight.BOLD), title_wrapped, 50) @@ -132,13 +112,14 @@ class BigConfirmationDialogV2(BigDialogBase): class BigInputDialog(BigDialogBase): BACK_TOUCH_AREA_PERCENTAGE = 0.2 BACKSPACE_RATE = 25 # hz + TEXT_INPUT_SIZE = 35 def __init__(self, hint: str, default_text: str = "", minimum_length: int = 1, confirm_callback: Callable[[str], None] | None = None): - super().__init__(None, None) + super().__init__() self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)), font_weight=FontWeight.MEDIUM) self._keyboard = MiciKeyboard() @@ -147,10 +128,11 @@ class BigInputDialog(BigDialogBase): self._backspace_held_time: float | None = None - self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 44, 44) + self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 42, 36) self._backspace_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) - self._enter_img = gui_app.texture("icons_mici/settings/keyboard/confirm.png", 44, 44) + self._enter_img = gui_app.texture("icons_mici/settings/keyboard/enter.png", 76, 62) + self._enter_disabled_img = gui_app.texture("icons_mici/settings/keyboard/enter_disabled.png", 76, 62) self._enter_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) # rects for top buttons @@ -179,60 +161,51 @@ class BigInputDialog(BigDialogBase): self._backspace_held_time = None def _render(self, _): - text_input_size = 35 - # draw current text so far below everything. text floats left but always stays in view text = self._keyboard.text() candidate_char = self._keyboard.get_candidate_character() - text_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), text + candidate_char or self._hint_label.text, text_input_size) - text_x = PADDING * 2 + self._enter_img.width + text_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), text + candidate_char or self._hint_label.text, self.TEXT_INPUT_SIZE) - # text needs to move left if we're at the end where right button is - text_rect = rl.Rectangle(text_x, - int(self._rect.y + PADDING), - # clip width to right button when in view - int(self._rect.width - text_x - PADDING * 2 - self._enter_img.width + 5), # TODO: why 5? - int(text_size.y)) - - # draw rounded background for text input bg_block_margin = 5 - text_field_rect = rl.Rectangle(text_rect.x - bg_block_margin, text_rect.y - bg_block_margin, - text_rect.width + bg_block_margin * 2, text_input_size + bg_block_margin * 2) + text_x = PADDING / 2 + self._enter_img.width + PADDING + text_field_rect = rl.Rectangle(text_x, int(self._rect.y + PADDING) - bg_block_margin, + int(self._rect.width - text_x * 2), + int(text_size.y)) # draw text input # push text left with a gradient on left side if too long - if text_size.x > text_rect.width: - text_x -= text_size.x - text_rect.width + if text_size.x > text_field_rect.width: + text_x -= text_size.x - text_field_rect.width - rl.begin_scissor_mode(int(text_rect.x), int(text_rect.y), int(text_rect.width), int(text_rect.height)) - rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), text, rl.Vector2(text_x, text_rect.y), text_input_size, 0, rl.WHITE) + rl.begin_scissor_mode(int(text_field_rect.x), int(text_field_rect.y), int(text_field_rect.width), int(text_field_rect.height)) + rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), text, rl.Vector2(text_x, text_field_rect.y), self.TEXT_INPUT_SIZE, 0, rl.WHITE) # draw grayed out character user is hovering over if candidate_char: - candidate_char_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), candidate_char, text_input_size) + candidate_char_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), candidate_char, self.TEXT_INPUT_SIZE) rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), candidate_char, - rl.Vector2(min(text_x + text_size.x, text_rect.x + text_rect.width) - candidate_char_size.x, text_rect.y), - text_input_size, 0, rl.Color(255, 255, 255, 128)) + rl.Vector2(min(text_x + text_size.x, text_field_rect.x + text_field_rect.width) - candidate_char_size.x, text_field_rect.y), + self.TEXT_INPUT_SIZE, 0, rl.Color(255, 255, 255, 128)) rl.end_scissor_mode() # draw gradient on left side to indicate more text - if text_size.x > text_rect.width: - rl.draw_rectangle_gradient_h(int(text_rect.x), int(text_rect.y), 80, int(text_rect.height), + if text_size.x > text_field_rect.width: + rl.draw_rectangle_gradient_h(int(text_field_rect.x), int(text_field_rect.y), 80, int(text_field_rect.height), rl.BLACK, rl.BLANK) # draw cursor if text: blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2 - cursor_x = min(text_x + text_size.x + 3, text_rect.x + text_rect.width) - rl.draw_rectangle_rounded(rl.Rectangle(int(cursor_x), int(text_rect.y), 4, int(text_size.y)), + cursor_x = min(text_x + text_size.x + 3, text_field_rect.x + text_field_rect.width) + rl.draw_rectangle_rounded(rl.Rectangle(int(cursor_x), int(text_field_rect.y), 4, int(text_size.y)), 1, 4, rl.Color(255, 255, 255, int(255 * blink_alpha))) # draw backspace icon with nice fade self._backspace_img_alpha.update(255 * bool(text)) if self._backspace_img_alpha.x > 1: color = rl.Color(255, 255, 255, int(self._backspace_img_alpha.x)) - rl.draw_texture(self._backspace_img, int(self._rect.width - self._enter_img.width - 15), int(text_field_rect.y), color) + rl.draw_texture(self._backspace_img, int(self._rect.width - self._backspace_img.width - 27), int(self._rect.y + 14), color) if not text and self._hint_label.text and not candidate_char: # draw description if no text entered yet and not drawing candidate char @@ -244,10 +217,12 @@ class BigInputDialog(BigDialogBase): self._top_right_button_rect = rl.Rectangle(text_field_rect.x + text_field_rect.width, self._rect.y, self._rect.width - (text_field_rect.x + text_field_rect.width), self._top_left_button_rect.height) - self._enter_img_alpha.update(255 if (len(text) >= self._minimum_length) else 255 * 0.35) - if self._enter_img_alpha.x > 1: - color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x)) - rl.draw_texture(self._enter_img, int(self._rect.x + 15), int(text_field_rect.y), color) + # draw enter button + self._enter_img_alpha.update(255 if len(text) >= self._minimum_length else 0) + color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x)) + rl.draw_texture(self._enter_img, int(self._rect.x + PADDING / 2), int(self._rect.y), color) + color = rl.Color(255, 255, 255, 255 - int(self._enter_img_alpha.x)) + rl.draw_texture(self._enter_disabled_img, int(self._rect.x + PADDING / 2), int(self._rect.y), color) # keyboard goes over everything self._keyboard.render(self._rect) @@ -255,7 +230,6 @@ class BigInputDialog(BigDialogBase): # draw debugging rect bounds if DEBUG: rl.draw_rectangle_lines_ex(text_field_rect, 1, rl.Color(100, 100, 100, 255)) - rl.draw_rectangle_lines_ex(text_rect, 1, rl.Color(0, 255, 0, 255)) rl.draw_rectangle_lines_ex(self._top_right_button_rect, 1, rl.Color(0, 255, 0, 255)) rl.draw_rectangle_lines_ex(self._top_left_button_rect, 1, rl.Color(0, 255, 0, 255)) @@ -316,9 +290,8 @@ class BigDialogOptionButton(Widget): class BigMultiOptionDialog(BigDialogBase): BACK_TOUCH_AREA_PERCENTAGE = 0.1 - def __init__(self, options: list[str], default: str | None, - right_btn: str | None = 'check', right_btn_callback: Callable[[], None] | None = None): - super().__init__(right_btn, right_btn_callback=right_btn_callback) + def __init__(self, options: list[str], default: str | None): + super().__init__() self._options = options if default is not None: assert default in options @@ -331,8 +304,6 @@ class BigMultiOptionDialog(BigDialogBase): self._can_click = True self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0, snap_items=True) - if self._right_btn is not None: - self._scroller.set_enabled(lambda: not cast(Widget, self._right_btn).is_pressed) for option in options: self._scroller.add_widget(BigDialogOptionButton(option)) diff --git a/selfdrive/ui/mici/widgets/pairing_dialog.py b/selfdrive/ui/mici/widgets/pairing_dialog.py index e064205d59..88bab2d001 100644 --- a/selfdrive/ui/mici/widgets/pairing_dialog.py +++ b/selfdrive/ui/mici/widgets/pairing_dialog.py @@ -24,7 +24,7 @@ class PairingDialog(NavWidget): self._qr_texture: rl.Texture | None = None self._last_qr_generation = float("-inf") - self._txt_pair = gui_app.texture("icons_mici/settings/device/pair.png", 84, 64) + self._txt_pair = gui_app.texture("icons_mici/settings/device/pair.png", 33, 60) self._pair_label = MiciLabel("pair with comma connect", 48, font_weight=FontWeight.BOLD, color=rl.Color(255, 255, 255, int(255 * 0.9)), line_height=40, wrap_text=True) diff --git a/selfdrive/ui/mici/widgets/side_button.py b/selfdrive/ui/mici/widgets/side_button.py deleted file mode 100644 index 4803b6d208..0000000000 --- a/selfdrive/ui/mici/widgets/side_button.py +++ /dev/null @@ -1,31 +0,0 @@ -import pyray as rl -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.lib.application import gui_app - -# --------------------------------------------------------------------------- -# Constants extracted from the original Qt style -# --------------------------------------------------------------------------- -# TODO: this should be corrected, but Scroller relies on this being incorrect :/ -WIDTH, HEIGHT = 112, 240 - - -class SideButton(Widget): - def __init__(self, btn_type: str): - super().__init__() - self.type = btn_type - self.set_rect(rl.Rectangle(0, 0, WIDTH, HEIGHT)) - - # load pre-rendered button images - if btn_type not in ("check", "back"): - btn_type = "back" - btn_img_path = f"icons_mici/buttons/button_side_{btn_type}.png" - btn_img_pressed_path = f"icons_mici/buttons/button_side_{btn_type}_pressed.png" - self._txt_btn, self._txt_btn_back = gui_app.texture(btn_img_path, 100, 224), gui_app.texture(btn_img_pressed_path, 100, 224) - - def _render(self, _) -> bool: - x = int(self._rect.x + 12) - y = int(self._rect.y + (self._rect.height - self._txt_btn.height) / 2) - rl.draw_texture(self._txt_btn if not self.is_pressed else self._txt_btn_back, - x, y, rl.WHITE) - - return False diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/selfdrive/ui/onroad/augmented_road_view.py index bcbcb2dcfb..76e7b078d1 100644 --- a/selfdrive/ui/onroad/augmented_road_view.py +++ b/selfdrive/ui/onroad/augmented_road_view.py @@ -15,7 +15,7 @@ from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCamera from openpilot.common.transformations.orientation import rot_from_euler if gui_app.sunnypilot_ui(): - from openpilot.selfdrive.ui.sunnypilot.onroad.augmented_road_view import BORDER_COLORS_SP + from openpilot.selfdrive.ui.sunnypilot.onroad.augmented_road_view import BORDER_COLORS_SP, AugmentedRoadViewSP from openpilot.selfdrive.ui.sunnypilot.onroad.driver_state import DriverStateRendererSP as DriverStateRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.hud_renderer import HudRendererSP as HudRenderer from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus @@ -38,9 +38,10 @@ ROAD_CAM_MIN_SPEED = 15.0 # m/s (34 mph) INF_POINT = np.array([1000.0, 0.0, 0.0]) -class AugmentedRoadView(CameraView): +class AugmentedRoadView(CameraView, AugmentedRoadViewSP): def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD): - super().__init__("camerad", stream_type) + CameraView.__init__(self, "camerad", stream_type) + AugmentedRoadViewSP.__init__(self) self._set_placeholder_color(BORDER_COLORS[UIStatus.DISENGAGED]) self.device_camera: DeviceCameraConfig | None = None @@ -92,6 +93,7 @@ class AugmentedRoadView(CameraView): # Draw all UI overlays self.model_renderer.render(self._content_rect) + AugmentedRoadViewSP.update_fade_out_bottom_overlay(self, self._content_rect) self._hud_renderer.render(self._content_rect) self.alert_renderer.render(self._content_rect) self.driver_state_renderer.render(self._content_rect) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise.py b/selfdrive/ui/sunnypilot/layouts/settings/cruise.py index 9439c588d3..671174ac7a 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/cruise.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/cruise.py @@ -4,27 +4,190 @@ 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.system.ui.widgets.scroller_tici import Scroller +from enum import IntEnum + +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise_sub_layouts.speed_limit_settings import SpeedLimitSettingsLayout +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.multilang import tr, tr_noop +from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp, simple_button_item_sp from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.scroller_tici import Scroller + + +class PanelType(IntEnum): + CRUISE = 0 + SLA = 1 + + +ICBM_DESC = tr_noop("When enabled, sunnypilot will attempt to manage the built-in cruise control buttons " + + "by emulating button presses for limited longitudinal control.") +ICMB_UNAVAILABLE = tr_noop("Intelligent Cruise Button Management is currently unavailable on this platform.") +ICMB_UNAVAILABLE_LONG_AVAILABLE = tr_noop("Disable the sunnypilot Longitudinal Control (alpha) toggle to allow Intelligent Cruise Button Management.") +ICMB_UNAVAILABLE_LONG_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control is the default longitudinal control for this platform.") + +ACC_ENABLED_DESCRIPTION = tr_noop("Enable custom Short & Long press increments for cruise speed increase/decrease.") +ACC_NOLONG_DESCRIPTION = tr_noop("This feature can only be used with sunnypilot longitudinal control enabled.") +ACC_PCMCRUISE_DISABLED_DESCRIPTION = tr_noop("This feature is not supported on this platform due to vehicle limitations.") +ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.") class CruiseLayout(Widget): def __init__(self): super().__init__() + self._current_panel = PanelType.CRUISE + self._speed_limit_layout = SpeedLimitSettingsLayout(lambda: self._set_current_panel(PanelType.CRUISE)) - self._params = Params() items = self._initialize_items() self._scroller = Scroller(items, line_separator=True, spacing=0) def _initialize_items(self): + self.icbm_toggle = toggle_item_sp( + title=tr("Intelligent Cruise Button Management (ICBM) (Alpha)"), + description="", + param="IntelligentCruiseButtonManagement") + + self.scc_v_toggle = toggle_item_sp( + title=tr("Smart Cruise Control - Vision"), + description=tr("Use vision path predictions to estimate the appropriate speed to drive through turns ahead."), + param="SmartCruiseControlVision") + + self.scc_m_toggle = toggle_item_sp( + title=tr("Smart Cruise Control - Map"), + description=tr("Use map data to estimate the appropriate speed to drive through turns ahead."), + param="SmartCruiseControlMap") + + self.custom_acc_toggle = toggle_item_sp( + title=tr("Custom ACC Speed Increments"), + description="", + param="CustomAccIncrementsEnabled", + callback=self._on_custom_acc_toggle) + + self.custom_acc_short_increment = option_item_sp( + title=tr("Short Press Increment"), + param="CustomAccShortPressIncrement", + min_value=1, max_value=10, value_change_step=1, + inline=True) + + self.custom_acc_long_increment = option_item_sp( + title=tr("Long Press Increment"), + param="CustomAccLongPressIncrement", + value_map={1: 1, 2: 5, 3: 10}, + min_value=1, max_value=3, value_change_step=1, + inline=True) + + self.sla_settings_button = simple_button_item_sp( + button_text=lambda: tr("Speed Limit"), + button_width=800, + callback=lambda: self._set_current_panel(PanelType.SLA) + ) + + self.dec_toggle = toggle_item_sp( + title=tr("Enable Dynamic Experimental Control"), + description=tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."), + param="DynamicExperimentalControl") + items = [ + self.icbm_toggle, + self.dec_toggle, + self.scc_v_toggle, + self.scc_m_toggle, + self.custom_acc_toggle, + self.custom_acc_short_increment, + self.custom_acc_long_increment, + self.sla_settings_button, ] return items def _render(self, rect): - self._scroller.render(rect) + if self._current_panel == PanelType.SLA: + self._speed_limit_layout.render(rect) + else: + self._scroller.render(rect) def show_event(self): + self._set_current_panel(PanelType.CRUISE) self._scroller.show_event() + self.icbm_toggle.show_description(True) + self.custom_acc_toggle.show_description(True) + + def _set_current_panel(self, panel: PanelType): + self._current_panel = panel + if panel == PanelType.SLA: + self._speed_limit_layout.show_event() + + def _update_state(self): + super()._update_state() + + if ui_state.CP is not None and ui_state.CP_SP is not None: + has_icbm = ui_state.has_icbm + has_long = ui_state.has_longitudinal_control + + if ui_state.CP_SP.intelligentCruiseButtonManagementAvailable and not has_long: + self.icbm_toggle.action_item.set_enabled(ui_state.is_offroad()) + self.icbm_toggle.set_description(tr(ICBM_DESC)) + else: + ui_state.params.remove("IntelligentCruiseButtonManagement") + self.icbm_toggle.action_item.set_enabled(False) + + long_desc = ICMB_UNAVAILABLE + if has_long: + if ui_state.CP.alphaLongitudinalAvailable: + long_desc += " " + ICMB_UNAVAILABLE_LONG_AVAILABLE + else: + long_desc += " " + ICMB_UNAVAILABLE_LONG_UNAVAILABLE + + new_desc = "" + tr(long_desc) + "\n\n" + tr(ICBM_DESC) + if self.icbm_toggle.description != new_desc: + self.icbm_toggle.set_description(new_desc) + self.icbm_toggle.show_description(True) + + if has_long or has_icbm: + self.custom_acc_toggle.action_item.set_enabled(((has_long and not ui_state.CP.pcmCruise) or has_icbm) and ui_state.is_offroad()) + self.dec_toggle.action_item.set_enabled(has_long) + self.scc_v_toggle.action_item.set_enabled(True) + self.scc_m_toggle.action_item.set_enabled(True) + else: + ui_state.params.remove("CustomAccIncrementsEnabled") + ui_state.params.remove("DynamicExperimentalControl") + ui_state.params.remove("SmartCruiseControlVision") + ui_state.params.remove("SmartCruiseControlMap") + self.custom_acc_toggle.action_item.set_enabled(False) + self.dec_toggle.action_item.set_enabled(False) + self.scc_v_toggle.action_item.set_enabled(False) + self.scc_m_toggle.action_item.set_enabled(False) + + else: + has_icbm = has_long = False + self.icbm_toggle.action_item.set_enabled(False) + self.icbm_toggle.set_description(tr(ONROAD_ONLY_DESCRIPTION)) + + show_custom_acc_desc = False + + if ui_state.is_offroad(): + new_custom_acc_desc = tr(ONROAD_ONLY_DESCRIPTION) + show_custom_acc_desc = True + else: + if has_long or has_icbm: + if has_long and ui_state.CP.pcmCruise: + new_custom_acc_desc = tr(ACC_PCMCRUISE_DISABLED_DESCRIPTION) + show_custom_acc_desc = True + else: + new_custom_acc_desc = tr(ACC_ENABLED_DESCRIPTION) + else: + new_custom_acc_desc = tr(ACC_NOLONG_DESCRIPTION) + show_custom_acc_desc = True + self.custom_acc_toggle.action_item.set_state(False) + + if self.custom_acc_toggle.description != new_custom_acc_desc: + self.custom_acc_toggle.set_description(new_custom_acc_desc) + if show_custom_acc_desc: + self.custom_acc_toggle.show_description(True) + + self._on_custom_acc_toggle(self.custom_acc_toggle.action_item.get_state()) + + def _on_custom_acc_toggle(self, state): + self.custom_acc_short_increment.set_visible(state) + self.custom_acc_long_increment.set_visible(state) + self.custom_acc_short_increment.action_item.set_enabled(self.custom_acc_toggle.action_item.enabled) + self.custom_acc_long_increment.action_item.set_enabled(self.custom_acc_toggle.action_item.enabled) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_policy.py b/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_policy.py new file mode 100644 index 0000000000..e6846744a2 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_policy.py @@ -0,0 +1,65 @@ +""" +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 collections.abc import Callable + +import pyray as rl +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp +from openpilot.system.ui.widgets.network import NavButton +from openpilot.system.ui.widgets.scroller_tici import Scroller +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.sunnypilot.widgets import get_highlighted_description + +SPEED_LIMIT_POLICY_BUTTONS = [tr("Car Only"), tr("Map Only"), tr("Car First"), tr("Map First"), tr("Combined")] + +SPEED_LIMIT_POLICY_DESCRIPTIONS = [ + tr("Car Only: Use Speed Limit data only from Car"), + tr("Map Only: Use Speed Limit data only from OpenStreetMaps"), + tr("Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps"), + tr("Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car"), + tr("Combined: Use combined Speed Limit data from Car & OpenStreetMaps") +] + + +class SpeedLimitPolicyLayout(Widget): + def __init__(self, back_btn_callback: Callable): + super().__init__() + self._back_button = NavButton(tr("Back")) + self._back_button.set_click_callback(back_btn_callback) + + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=False, spacing=0) + + def _initialize_items(self): + self._speed_limit_policy = multiple_button_item_sp( + title=lambda: tr("Speed Limit Source"), + description=self._get_policy_description, + buttons=SPEED_LIMIT_POLICY_BUTTONS, + param="SpeedLimitPolicy", + button_width=250, + ) + + items = [ + self._speed_limit_policy + ] + return items + + @staticmethod + def _get_policy_description(): + return get_highlighted_description(ui_state.params, "SpeedLimitPolicy", SPEED_LIMIT_POLICY_DESCRIPTIONS) + + def _render(self, rect): + self._back_button.set_position(self._rect.x, self._rect.y + 20) + self._back_button.render() + + content_rect = rl.Rectangle(rect.x, rect.y + self._back_button.rect.height + 40, rect.width, rect.height - self._back_button.rect.height - 40) + self._scroller.render(content_rect) + + def show_event(self): + self._scroller.show_event() + self._speed_limit_policy.show_description(True) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_settings.py b/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_settings.py new file mode 100644 index 0000000000..c14330d9ac --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_settings.py @@ -0,0 +1,178 @@ +""" +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 collections.abc import Callable +from enum import IntEnum + +import pyray as rl +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise_sub_layouts.speed_limit_policy import SpeedLimitPolicyLayout +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import OffsetType as SpeedLimitOffsetType +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.sunnypilot.widgets import get_highlighted_description +from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp, option_item_sp, simple_button_item_sp, LineSeparatorSP +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.network import NavButton +from openpilot.system.ui.widgets.scroller_tici import Scroller + +SPEED_LIMIT_MODE_BUTTONS = [tr("Off"), tr("Info"), tr("Warning"), tr("Assist")] +SPEED_LIMIT_OFFSET_TYPE_BUTTONS = [tr("None"), tr("Fixed"), tr("%")] + +SPEED_LIMIT_MODE_DESCRIPTIONS = [ + tr("Off: Disables the Speed Limit functions."), + tr("Information: Displays the current road's speed limit."), + tr("Warning: Provides a warning when exceeding the current road's speed limit."), + tr("Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons."), +] + +SPEED_LIMIT_OFFSET_DESCRIPTIONS = [ + tr("None: No Offset"), + tr("Fixed: Adds a fixed offset [Speed Limit + Offset]"), + tr("Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)]"), +] + + +class PanelType(IntEnum): + SETTINGS = 0 + POLICY = 1 + + +class SpeedLimitSettingsLayout(Widget): + def __init__(self, back_btn_callback: Callable): + super().__init__() + self._current_panel = PanelType.SETTINGS + + self._back_button = NavButton(tr("Back")) + self._back_button.set_click_callback(back_btn_callback) + + self._policy_layout = SpeedLimitPolicyLayout(lambda: self._set_current_panel(PanelType.SETTINGS)) + + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=False, spacing=0) + + def _initialize_items(self): + self._speed_limit_mode = multiple_button_item_sp( + title=lambda: tr("Speed Limit"), + description=self._get_mode_description, + buttons=SPEED_LIMIT_MODE_BUTTONS, + param="SpeedLimitMode", + button_width=380, + ) + + self._source_button = simple_button_item_sp( + button_text=lambda: tr("Customize Source"), + button_width=720, + callback=lambda: self._set_current_panel(PanelType.POLICY) + ) + + self._speed_limit_offset_type = multiple_button_item_sp( + title=lambda: tr("Speed Limit Offset"), + description="", + buttons=SPEED_LIMIT_OFFSET_TYPE_BUTTONS, + param="SpeedLimitOffsetType", + button_width=450, + ) + + self._speed_limit_value_offset = option_item_sp( + title="", + param="SpeedLimitValueOffset", + min_value=-30, + max_value=30, + description=self._get_offset_description, + label_callback=self._get_offset_label, + ) + + items = [ + self._speed_limit_mode, + LineSeparatorSP(40), + self._source_button, + LineSeparatorSP(40), + self._speed_limit_offset_type, + self._speed_limit_value_offset + ] + return items + + def _set_current_panel(self, panel: PanelType): + self._current_panel = panel + if panel == PanelType.POLICY: + self._policy_layout.show_event() + + @staticmethod + def _get_mode_description(): + return get_highlighted_description(ui_state.params, "SpeedLimitMode", SPEED_LIMIT_MODE_DESCRIPTIONS) + + @staticmethod + def _get_offset_description(): + return get_highlighted_description(ui_state.params, "SpeedLimitOffsetType", SPEED_LIMIT_OFFSET_DESCRIPTIONS) + + @staticmethod + def _get_offset_label(value): + offset_type = int(ui_state.params.get("SpeedLimitOffsetType", return_default=True)) + unit = tr("km/h") if ui_state.is_metric else tr("mph") + + if offset_type == int(SpeedLimitOffsetType.percentage): + return f"{value}%" + elif offset_type == int(SpeedLimitOffsetType.fixed): + return f"{value} {unit}" + return str(value) + + def _update_state(self): + super()._update_state() + + speed_limit_mode_param = ui_state.params.get("SpeedLimitMode", return_default=True) + if ui_state.CP is not None and ui_state.CP_SP is not None: + brand = ui_state.CP.brand + has_long = ui_state.has_longitudinal_control + has_icbm = ui_state.has_icbm + + """ + Speed Limit Assist is available when: + - has_long or has_icbm, and + - is not a release branch or not a disallowed brand, and + - is not always disallwed + """ + sla_disallow_in_release = brand == "tesla" and ui_state.is_sp_release + sla_always_disallow = brand == "rivian" + sla_available = (has_long or has_icbm) and not sla_disallow_in_release and not sla_always_disallow + + if not sla_available and speed_limit_mode_param == int(SpeedLimitMode.assist): + ui_state.params.put("SpeedLimitMode", int(SpeedLimitMode.warning)) + + else: + sla_available = False + + if not sla_available: + self._speed_limit_mode.action_item.set_enabled_buttons({ + int(SpeedLimitMode.off), + int(SpeedLimitMode.information), + int(SpeedLimitMode.warning), + }) + else: + self._speed_limit_mode.action_item.set_enabled_buttons(None) + + offset_type = ui_state.params.get("SpeedLimitOffsetType", return_default=True) + self._speed_limit_value_offset.set_visible(offset_type != int(SpeedLimitOffsetType.off)) + + def _render(self, rect): + if self._current_panel == PanelType.POLICY: + self._policy_layout.render(rect) + return + + self._back_button.set_position(self._rect.x, self._rect.y + 20) + self._back_button.render() + + content_rect = rl.Rectangle(rect.x, rect.y + self._back_button.rect.height + 40, rect.width, rect.height - self._back_button.rect.height - 40) + self._scroller.render(content_rect) + + def show_event(self): + self._current_panel = PanelType.SETTINGS + self._scroller.show_event() + self._speed_limit_mode.show_description(True) + + def hide_event(self): + self._current_panel = PanelType.SETTINGS + self._scroller.hide_event() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering.py b/selfdrive/ui/sunnypilot/layouts/settings/steering.py index f82c4097c3..14d840138e 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/steering.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering.py @@ -72,6 +72,15 @@ class SteeringLayout(Widget): description="", label_callback=lambda speed: f'{speed} {"km/h" if ui_state.is_metric else "mph"}', ) + self._blinker_reengage_delay = option_item_sp( + param="BlinkerLateralReengageDelay", + title=lambda: tr("Post-Blinker Delay"), + min_value=0, + max_value=10, + value_change_step=1, + description=lambda: tr("Delay before lateral control resumes after the turn signal ends."), + label_callback=lambda delay: f'{delay} {"s"}' + ) self._torque_control_toggle = toggle_item_sp( param="EnforceTorqueControl", title=lambda: tr("Enforce Torque Lateral Control"), @@ -96,6 +105,7 @@ class SteeringLayout(Widget): LineSeparatorSP(40), self._blinker_control_toggle, self._blinker_control_options, + self._blinker_reengage_delay, LineSeparatorSP(40), self._torque_control_toggle, self._torque_customization_button, @@ -128,6 +138,7 @@ class SteeringLayout(Widget): self._mads_toggle.action_item.set_enabled(ui_state.is_offroad()) self._mads_settings_button.action_item.set_enabled(ui_state.is_offroad() and self._mads_toggle.action_item.get_state()) self._blinker_control_options.set_visible(self._blinker_control_toggle.action_item.get_state()) + self._blinker_reengage_delay.set_visible(self._blinker_control_toggle.action_item.get_state()) enforce_torque_enabled = self._torque_control_toggle.action_item.get_state() nnlc_enabled = self._nnlc_toggle.action_item.get_state() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py index d3f38e04d1..3542adf285 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py @@ -9,6 +9,7 @@ import pyray as rl from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.network import NavButton @@ -112,7 +113,7 @@ class MadsSettingsLayout(Widget): if self._mads_limited_settings(): ui_state.params.remove("MadsMainCruiseAllowed") ui_state.params.put_bool("MadsUnifiedEngagementMode", True) - ui_state.params.put("MadsSteeringMode", 2) + ui_state.params.put("MadsSteeringMode", MadsSteeringModeOnBrake.DISENGAGE) self._main_cruise_toggle.action_item.set_enabled(False) self._main_cruise_toggle.action_item.set_state(False) @@ -122,9 +123,9 @@ class MadsSettingsLayout(Widget): self._unified_engagement_toggle.action_item.set_state(True) self._unified_engagement_toggle.set_description("" + DEFAULT_TO_ON + "
" + MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC) - self._steering_mode.action_item.set_enabled(False) self._steering_mode.set_description(STATUS_DISENGAGE_ONLY) - self._steering_mode.action_item.set_selected_button(2) + self._steering_mode.action_item.set_selected_button(MadsSteeringModeOnBrake.DISENGAGE) + self._steering_mode.action_item.set_enabled_buttons({MadsSteeringModeOnBrake.DISENGAGE}) else: self._main_cruise_toggle.action_item.set_enabled(True) self._main_cruise_toggle.set_description(MADS_MAIN_CRUISE_BASE_DESC) @@ -133,3 +134,4 @@ class MadsSettingsLayout(Widget): self._unified_engagement_toggle.set_description(MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC) self._steering_mode.action_item.set_enabled(True) + self._steering_mode.action_item.set_enabled_buttons(None) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py b/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py index 00baf0cccf..7955f13202 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py @@ -355,5 +355,10 @@ class SunnylinkLayout(Widget): def show_event(self): super().show_event() + ui_state.sunnylink_state.set_settings_open(True) self._scroller.show_event() self._sunnylink_description.set_visible(False) + + def hide_event(self): + super().hide_event() + ui_state.sunnylink_state.set_settings_open(False) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/trips.py b/selfdrive/ui/sunnypilot/layouts/settings/trips.py index a318cebeb0..b389892d5d 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/trips.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/trips.py @@ -4,27 +4,148 @@ 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. """ +import requests +import threading +import time +import pyray as rl + +from openpilot.common.api import api_get +from openpilot.common.constants import CV from openpilot.common.params import Params -from openpilot.system.ui.widgets.scroller_tici import Scroller +from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.ui.lib.api_helpers import get_token +from openpilot.selfdrive.ui.ui_state import ui_state, device +from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget class TripsLayout(Widget): + PARAM_KEY = "ApiCache_DriveStats" + UPDATE_INTERVAL = 30 # seconds + def __init__(self): super().__init__() - self._params = Params() - items = self._initialize_items() - self._scroller = Scroller(items, line_separator=True, spacing=0) + self._session = requests.Session() + self._stats = self._get_stats() - def _initialize_items(self): - items = [ + self._icon_distance = gui_app.texture("icons/road.png", 100, 100, keep_aspect_ratio=True) + self._icon_drives = gui_app.texture("icons_mici/wheel.png", 80, 80, keep_aspect_ratio=True) + self._icon_hours = gui_app.texture("../../sunnypilot/selfdrive/assets/icons/clock.png", 80, 80, keep_aspect_ratio=True) - ] - return items + self._running = True + self._update_thread = threading.Thread(target=self._update_loop, daemon=True) + self._update_thread.start() - def _render(self, rect): - self._scroller.render(rect) + def __del__(self): + self._running = False + try: + if self._update_thread and self._update_thread.is_alive(): + self._update_thread.join(timeout=1.0) + except Exception: + pass - def show_event(self): - self._scroller.show_event() + def _get_stats(self): + stats = self._params.get(self.PARAM_KEY) + if not stats: + return {} + try: + return stats + except Exception: + cloudlog.exception(f"Failed to decode drive stats: {stats}") + return {} + + def _fetch_drive_stats(self): + try: + dongle_id = self._params.get("DongleId") + if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: + return + identity_token = get_token(dongle_id) + response = api_get(f"v1.1/devices/{dongle_id}/stats", access_token=identity_token, session=self._session) + if response.status_code == 200: + data = response.json() + self._stats = data + self._params.put(self.PARAM_KEY, data) + except Exception as e: + cloudlog.error(f"Failed to fetch drive stats: {e}") + + def _update_loop(self): + while self._running: + if not ui_state.started and device._awake: + self._fetch_drive_stats() + time.sleep(self.UPDATE_INTERVAL) + + def _render_stat_group(self, x, y, width, height, title, data, is_metric): + # Card Background + rl.draw_rectangle_rounded(rl.Rectangle(x, y, width, height), 0.05, 10, rl.Color(30, 30, 30, 255)) + + # Title + title_font = gui_app.font(FontWeight.BOLD) + rl.draw_text_ex(title_font, title, rl.Vector2(x + 60, y + 30), 50 * FONT_SCALE, 0, rl.Color(200, 200, 200, 255)) + + # Internal content area + # Center the content block (Icon + Value + Unit) vertically + content_y = y + (height / 2) - (140 * FONT_SCALE) + col_width = width / 3 + + # Values + number_font = gui_app.font(FontWeight.BOLD) + unit_font = gui_app.font(FontWeight.LIGHT) + number_base_size = 92 + unit_base_size = 55 + number_size = number_base_size * FONT_SCALE + unit_size = unit_base_size * FONT_SCALE + color_unit = rl.Color(160, 160, 160, 255) + + routes = int(data.get("routes", 0)) + distance = data.get("distance", 0) + distance_str = str(int(distance * CV.MPH_TO_KPH)) if is_metric else str(int(distance)) + hours = int(data.get("minutes", 0) / 60) + + dist_unit = tr("KM") if is_metric else tr("Miles") + + def draw_col(col_idx, icon, value, unit): + col_x = x + (col_width * col_idx) + center_x = col_x + (col_width / 2) + + # Icon + icon_x = int(center_x - (icon.width / 2)) + icon_y = int(content_y + 60) + rl.draw_texture(icon, icon_x, icon_y, rl.WHITE) + + # Value + val_size = measure_text_cached(number_font, value, number_base_size) + rl.draw_text_ex(number_font, value, rl.Vector2(center_x - val_size.x / 1.65, content_y + 145 * FONT_SCALE), number_size, 0, rl.WHITE) + + # Unit + unit_size_vec = measure_text_cached(unit_font, unit, unit_base_size) + rl.draw_text_ex(unit_font, unit, rl.Vector2(center_x - unit_size_vec.x / 1.65, content_y + 255 * FONT_SCALE), unit_size, 0, color_unit) + + draw_col(0, self._icon_drives, str(routes), tr("Drives")) + draw_col(1, self._icon_distance, distance_str, dist_unit) + draw_col(2, self._icon_hours, str(hours), tr("Hours")) + + return y + height + + def _render(self, rect: rl.Rectangle): + x = rect.x + y = rect.y + w = rect.width + + spacing = 30 + available_h = rect.height - 30 + card_height = available_h / 2 + + is_metric = self._params.get_bool("IsMetric") + + all_time = self._stats.get("all", {}) + week = self._stats.get("week", {}) + + y = self._render_stat_group(x, y, w, card_height, tr("ALL TIME"), all_time, is_metric) + y += spacing + y = self._render_stat_group(x, y, w, card_height, tr("PAST WEEK"), week, is_metric) + + return -1 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/visuals.py b/selfdrive/ui/sunnypilot/layouts/settings/visuals.py index 1036af3e5f..84be5a26ab 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/visuals.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/visuals.py @@ -5,9 +5,18 @@ 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.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.multilang import tr, tr_noop +from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, multiple_button_item_sp from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets import Widget +CHEVRON_INFO_DESCRIPTION = { + "enabled": tr_noop("Display useful metrics below the chevron that tracks the lead car " + + "only applicable to cars with sunnypilot longitudinal control."), + "disabled": tr_noop("This feature requires sunnypilot longitudinal control to be available.") +} + class VisualsLayout(Widget): def __init__(self): @@ -18,13 +27,128 @@ class VisualsLayout(Widget): self._scroller = Scroller(items, line_separator=True, spacing=0) def _initialize_items(self): - items = [ + self._toggle_defs = { + "BlindSpot": ( + lambda: tr("Show Blind Spot Warnings"), + tr("Enabling this will display warnings when a vehicle is detected in your " + + "blind spot as long as your car has BSM supported."), + None, + ), + "TorqueBar": ( + lambda: tr("Steering Arc"), + tr("Display steering arc on the driving screen when lateral control is enabled."), + None, + ), + "RainbowMode": ( + lambda: tr("Enable Tesla Rainbow Mode"), + tr("A beautiful rainbow effect on the path the model wants to take. " + + "It does not affect driving in any way."), + None, + ), + "StandstillTimer": ( + lambda: tr("Enable Standstill Timer"), + tr("Show a timer on the HUD when the car is at a standstill."), + None, + ), + "RoadNameToggle": ( + lambda: tr("Display Road Name"), + tr("Displays the name of the road the car is traveling on." + + "
The OpenStreetMap database of the location must be downloaded from " + + "the OSM panel to fetch the road name."), + None, + ), + "GreenLightAlert": ( + lambda: tr("Green Traffic Light Alert (Beta)"), + tr("A chime and on-screen alert will play when the traffic light you are waiting for " + + "turns green and you have no vehicle in front of you." + + "
Note: This chime is only designed as a notification. " + + "It is the driver's responsibility to observe their environment and make decisions accordingly."), + None, + ), + "LeadDepartAlert": ( + lambda: tr("Lead Departure Alert (Beta)"), + tr("A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving." + + "
Note: This chime is only designed as a notification. " + + "It is the driver's responsibility to observe their environment and make decisions accordingly."), + None, + ), + "TrueVEgoUI": ( + lambda: tr("Speedometer: Always Display True Speed"), + tr("For applicable vehicles, always display the true vehicle current speed from wheel speed sensors."), + None, + ), + "HideVEgoUI": ( + lambda: tr("Speedometer: Hide from Onroad Screen"), + tr("When enabled, the speedometer on the onroad screen is not displayed."), + None, + ), + "ShowTurnSignals": ( + lambda: tr("Display Turn Signals"), + tr("When enabled, visual turn indicators are drawn on the HUD."), + None, + ), + "RocketFuel": ( + lambda: tr("Real-time Acceleration Bar"), + tr("Show an indicator on the left side of the screen to display real-time vehicle acceleration and deceleration. " + + "This displays what the car is currently doing, not what the planner is requesting."), + None, + ), + } + self._toggles = {} + for param, (title, desc, callback) in self._toggle_defs.items(): + toggle = toggle_item_sp( + title=title, + description=desc, + param=param, + initial_state=ui_state.params.get_bool(param), + callback=callback, + ) + self._toggles[param] = toggle + self._chevron_info = multiple_button_item_sp( + title=lambda: tr("Display Metrics Below Chevron"), + description="", + buttons=[lambda: tr("Off"), lambda: tr("Distance"), lambda: tr("Speed"), lambda: tr("Time"), lambda: tr("All")], + param="ChevronInfo", + inline=False + ) + self._dev_ui_info = multiple_button_item_sp( + title=lambda: tr("Developer UI"), + description=lambda: tr("Display real-time parameters and metrics from various sources."), + buttons=[lambda: tr("Off"), lambda: tr("Bottom"), lambda: tr("Right"), lambda: tr("Right & Bottom")], + param="DevUIInfo", + button_width=350, + inline=False + ) + + items = list(self._toggles.values()) + [ + self._chevron_info, + self._dev_ui_info, ] return items + def _update_state(self): + super()._update_state() + + for param in self._toggle_defs: + self._toggles[param].action_item.set_state(self._params.get_bool(param)) + + self._dev_ui_info.action_item.set_selected_button(ui_state.params.get("DevUIInfo", return_default=True)) + + if ui_state.has_longitudinal_control: + self._chevron_info.set_description(tr(CHEVRON_INFO_DESCRIPTION["enabled"])) + self._chevron_info.action_item.set_selected_button(ui_state.params.get("ChevronInfo", return_default=True)) + self._chevron_info.action_item.set_enabled(True) + else: + self._chevron_info.set_description(tr(CHEVRON_INFO_DESCRIPTION["disabled"])) + self._chevron_info.action_item.set_enabled(False) + ui_state.params.put("ChevronInfo", 0) + def _render(self, rect): self._scroller.render(rect) def show_event(self): self._scroller.show_event() + if not ui_state.has_longitudinal_control: + self._chevron_info.set_description(tr(CHEVRON_INFO_DESCRIPTION["disabled"])) + self._chevron_info.show_description(True) diff --git a/selfdrive/ui/sunnypilot/mici/layouts/models.py b/selfdrive/ui/sunnypilot/mici/layouts/models.py new file mode 100644 index 0000000000..d5964ea964 --- /dev/null +++ b/selfdrive/ui/sunnypilot/mici/layouts/models.py @@ -0,0 +1,122 @@ +""" +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 collections.abc import Callable + +from cereal import custom +from openpilot.selfdrive.ui.mici.widgets.button import BigButton +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.widgets import NavWidget, Widget +from openpilot.system.ui.widgets.scroller import Scroller + + +class ModelsLayoutMici(NavWidget): + def __init__(self, back_callback: Callable): + super().__init__() + self.set_back_callback(back_callback) + self.original_back_callback = back_callback + self.focused_widget = None + + self.current_model_btn = BigButton(tr("current model"), "", "") + self.current_model_btn.set_click_callback(self._show_folders) + + self.cancel_download_btn = BigButton(tr("cancel download"), "", "") + self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadIndex")) + + self.main_items: list[Widget] = [self.current_model_btn, self.cancel_download_btn] + self._scroller = Scroller(self.main_items, snap_items=False) + + @property + def model_manager(self): + return ui_state.sm["modelManagerSP"] + + def _get_grouped_bundles(self): + bundles = self.model_manager.availableBundles + folders = {} + for bundle in bundles: + folder = next((override.value for override in bundle.overrides if override.key == "folder"), "") + folders.setdefault(folder, []).append(bundle) + return folders + + def _show_selection_view(self, items: list[Widget], back_callback: Callable): + self._scroller._items = items + for item in items: + item.set_touch_valid_callback(lambda: self._scroller.scroll_panel.is_touch_valid() and self._scroller.enabled) + self._scroller.scroll_panel.set_offset(0) + self.set_back_callback(back_callback) + + def _show_folders(self): + self.focused_widget = self.current_model_btn + folders = self._get_grouped_bundles() + folder_buttons = [] + default_btn = BigButton(tr("default model"), "", "") + default_btn.set_click_callback(self._select_default) + folder_buttons.append(default_btn) + + for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True): + if folder.lower() in ["release models", "master models"]: + btn = BigButton(folder.lower(), "", "") + btn.set_click_callback(lambda f=folder: self._select_folder(f)) + folder_buttons.append(btn) + self._show_selection_view(folder_buttons, self._reset_main_view) + + def _select_model(self, bundle): + ui_state.params.put("ModelManager_DownloadIndex", bundle.index) + self._reset_main_view() + + def _select_default(self): + ui_state.params.remove("ModelManager_ActiveBundle") + self._reset_main_view() + + def _select_folder(self, folder_name): + folders = self._get_grouped_bundles() + bundles = sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True) + + btns = [] + for bundle in bundles: + txt = bundle.displayName.lower() + btn = BigButton(txt, "", "") + btn.set_click_callback(lambda b=bundle: self._select_model(b)) + btns.append(btn) + self._show_selection_view(btns, self._show_folders) + + def _reset_main_view(self): + self._scroller._items = self.main_items + self.set_back_callback(self.original_back_callback) + if self.focused_widget and self.focused_widget in self.main_items: + x = self._scroller._pad_start + for item in self.main_items: + if not item.is_visible: + continue + if item == self.focused_widget: + break + x += item.rect.width + self._scroller._spacing + self._scroller.scroll_panel.set_offset(0) + self._scroller.scroll_to(x) + self.focused_widget = None + else: + self._scroller.scroll_panel.set_offset(0) + + def _update_state(self): + super()._update_state() + + manager = self.model_manager + if manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading: + self.current_model_btn.set_value("downloading...") + self.cancel_download_btn.set_visible(True) + else: + self.current_model_btn.set_value(manager.activeBundle.internalName.lower() if manager.activeBundle else tr("default model")) + self.cancel_download_btn.set_visible(False) + self.current_model_btn.set_enabled(ui_state.is_offroad()) + self.current_model_btn.set_text(tr("current model")) + + def _render(self, rect): + self._scroller.render(rect) + + def show_event(self): + super().show_event() + self._scroller.show_event() diff --git a/selfdrive/ui/sunnypilot/mici/layouts/settings.py b/selfdrive/ui/sunnypilot/mici/layouts/settings.py index f6fae6630c..69982e2298 100644 --- a/selfdrive/ui/sunnypilot/mici/layouts/settings.py +++ b/selfdrive/ui/sunnypilot/mici/layouts/settings.py @@ -9,6 +9,7 @@ from enum import IntEnum from openpilot.selfdrive.ui.mici.layouts.settings import settings as OP from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici +from openpilot.selfdrive.ui.sunnypilot.mici.layouts.models import ModelsLayoutMici ICON_SIZE = 70 @@ -16,6 +17,7 @@ OP.PanelType = IntEnum( "PanelType", [es.name for es in OP.PanelType] + [ "SUNNYLINK", + "MODELS", ], start=0, ) @@ -27,13 +29,19 @@ class SettingsLayoutSP(OP.SettingsLayout): sunnylink_btn = BigButton("sunnylink", "", "icons_mici/settings/developer/ssh.png") sunnylink_btn.set_click_callback(lambda: self._set_current_panel(OP.PanelType.SUNNYLINK)) + + models_btn = BigButton("models", "", "../../sunnypilot/selfdrive/assets/offroad/icon_models.png") + models_btn.set_click_callback(lambda: self._set_current_panel(OP.PanelType.MODELS)) + self._panels.update({ OP.PanelType.SUNNYLINK: OP.PanelInfo("sunnylink", SunnylinkLayoutMici(back_callback=lambda: self._set_current_panel(None))), + OP.PanelType.MODELS: OP.PanelInfo("models", ModelsLayoutMici(back_callback=lambda: self._set_current_panel(None))), }) items = self._scroller._items.copy() items.insert(1, sunnylink_btn) + items.insert(2, models_btn) self._scroller._items.clear() for item in items: self._scroller.add_widget(item) diff --git a/selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py b/selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py index 5a718947cf..e3de9401ce 100644 --- a/selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py +++ b/selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py @@ -6,8 +6,14 @@ See the LICENSE.md file in the root directory for more details. """ import pyray as rl from openpilot.selfdrive.ui.ui_state import UIStatus +from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath LANE_LINE_COLORS_SP = { UIStatus.LAT_ONLY: rl.Color(0, 255, 64, 255), UIStatus.LONG_ONLY: rl.Color(0, 255, 64, 255), } + + +class ModelRendererSP: + def __init__(self): + self.rainbow_path = RainbowPath() diff --git a/selfdrive/ui/sunnypilot/onroad/augmented_road_view.py b/selfdrive/ui/sunnypilot/onroad/augmented_road_view.py index 0a5739cc00..64d7a3d946 100644 --- a/selfdrive/ui/sunnypilot/onroad/augmented_road_view.py +++ b/selfdrive/ui/sunnypilot/onroad/augmented_road_view.py @@ -5,9 +5,27 @@ 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 pyray as rl -from openpilot.selfdrive.ui.ui_state import UIStatus +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.selfdrive.ui.ui_state import UIStatus, ui_state +from openpilot.system.ui.lib.application import gui_app BORDER_COLORS_SP = { UIStatus.LAT_ONLY: rl.Color(0x00, 0xC8, 0xC8, 0xFF), # Cyan for lateral-only state UIStatus.LONG_ONLY: rl.Color(0x96, 0x1C, 0xA8, 0xFF), # Purple for longitudinal-only state } + + +class AugmentedRoadViewSP: + def __init__(self): + self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png") + self._fade_alpha_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) + + def update_fade_out_bottom_overlay(self, _content_rect): + # Fade out bottom of overlays for looks (only when engaged) + fade_alpha = self._fade_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED) + if ui_state.torque_bar and ui_state.sm['controlsState'].lateralControlState.which() != 'angleState' and fade_alpha > 1e-2: + # Scale the fade texture to the content rect + rl.draw_texture_pro(self._fade_texture, + rl.Rectangle(0, 0, self._fade_texture.width, self._fade_texture.height), + _content_rect, rl.Vector2(0, 0), 0.0, + rl.Color(255, 255, 255, int(255 * fade_alpha))) diff --git a/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py b/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py index 1087579fef..61aa52537b 100644 --- a/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py +++ b/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py @@ -31,6 +31,9 @@ class BlindSpotIndicators: return self._blind_spot_left_alpha_filter.x > 0.01 or self._blind_spot_right_alpha_filter.x > 0.01 def render(self, rect: rl.Rectangle) -> None: + if not ui_state.blindspot: + return + BLIND_SPOT_MARGIN_X = 20 # Distance from edge of screen BLIND_SPOT_Y_OFFSET = 100 # Distance from top of screen diff --git a/selfdrive/ui/sunnypilot/onroad/circular_alerts.py b/selfdrive/ui/sunnypilot/onroad/circular_alerts.py new file mode 100644 index 0000000000..8aa4c71d0d --- /dev/null +++ b/selfdrive/ui/sunnypilot/onroad/circular_alerts.py @@ -0,0 +1,140 @@ +""" +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. +""" +import pyray as rl + +from cereal import log +from openpilot.selfdrive.ui import UI_BORDER_SIZE +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE +from openpilot.system.ui.lib.text_measure import measure_text_cached + + +class CircularAlertsRenderer: + def __init__(self): + self._green_light_alert_img = gui_app.texture("../../sunnypilot/selfdrive/assets/images/green_light.png", 250, 250) + self._lead_depart_alert_img = gui_app.texture("../../sunnypilot/selfdrive/assets/images/lead_depart.png", 250, 250) + + self._e2e_alert_display_timer = 0 + self._e2e_alert_frame = 0 + self._green_light_alert = False + self._lead_depart_alert = False + self._standstill_elapsed_time = 0.0 + self._is_standstill = False + self._alert_text = "" + self._alert_img = None + self._allow_e2e_alerts = False + + def update(self) -> None: + sm = ui_state.sm + lp_sp = sm['longitudinalPlanSP'] + car_state = sm['carState'] + self._green_light_alert = lp_sp.e2eAlerts.greenLightAlert + self._lead_depart_alert = lp_sp.e2eAlerts.leadDepartAlert + self._is_standstill = car_state.standstill + + if not ui_state.started: + self._standstill_elapsed_time = 0.0 + + self._allow_e2e_alerts = sm['selfdriveState'].alertSize == log.SelfdriveState.AlertSize.none and \ + sm.recv_frame['driverStateV2'] > ui_state.started_frame + + if self._green_light_alert or self._lead_depart_alert: + self._e2e_alert_display_timer = 3 * gui_app.target_fps + # reset onroad sleep timer for e2e alerts + ui_state.reset_onroad_sleep_timer() + + if self._e2e_alert_display_timer > 0: + self._e2e_alert_frame += 1 + self._e2e_alert_display_timer -= 1 + + if self._green_light_alert: + self._alert_text = "GREEN\nLIGHT" + self._alert_img = self._green_light_alert_img + elif self._lead_depart_alert: + self._alert_text = "LEAD VEHICLE\nDEPARTING" + self._alert_img = self._lead_depart_alert_img + + elif ui_state.standstill_timer and self._is_standstill: + self._alert_img = None + self._standstill_elapsed_time += 1.0 / gui_app.target_fps + minute = int(self._standstill_elapsed_time / 60) + second = int(self._standstill_elapsed_time - (minute * 60)) + self._alert_text = f"{minute:01d}:{second:02d}" + self._e2e_alert_frame += 1 + + else: + self._e2e_alert_frame = 0 + if not self._is_standstill: + self._standstill_elapsed_time = 0.0 + + def render(self, rect: rl.Rectangle) -> None: + if not self._allow_e2e_alerts or (self._e2e_alert_display_timer <= 0 and not (ui_state.standstill_timer and self._is_standstill)): + return + + e2e_alert_size = 250 + dev_ui_width_adjustment = 180 if ui_state.developer_ui in (DeveloperUiRenderer.DEV_UI_RIGHT, DeveloperUiRenderer.DEV_UI_BOTH) else 100 + + x = rect.x + rect.width - e2e_alert_size - dev_ui_width_adjustment - (UI_BORDER_SIZE * 3) + y = rect.y + rect.height / 2 + 20 + + alert_rect = rl.Rectangle(x - e2e_alert_size, y - e2e_alert_size, e2e_alert_size * 2, e2e_alert_size * 2) + center = rl.Vector2(alert_rect.x + alert_rect.width / 2, alert_rect.y + alert_rect.height / 2) + + # Pulse logic + is_pulsing = (self._e2e_alert_frame % gui_app.target_fps) < (gui_app.target_fps / 2.5) + + # Standstill Timer (STOPPED) should be static white + if self._e2e_alert_display_timer == 0 and ui_state.standstill_timer and self._is_standstill: + frame_color = rl.Color(255, 255, 255, 75) + else: + frame_color = rl.Color(255, 255, 255, 75) if is_pulsing else rl.Color(0, 255, 0, 75) + + # Draw Circle + rl.draw_circle_v(center, e2e_alert_size, rl.Color(0, 0, 0, 190)) + # Draw Ring (Border) + rl.draw_ring(center, e2e_alert_size - 7.5, e2e_alert_size + 7.5, 0, 360, 0, frame_color) + + # Draw Image + if self._alert_img and self._e2e_alert_display_timer > 0: + img_x = int(center.x - self._alert_img.width / 2) + img_y = int(center.y - self._alert_img.height / 2) + rl.draw_texture(self._alert_img, img_x, img_y, rl.WHITE) + + # Draw Text + txt_color = rl.Color(255, 255, 255, 255) if is_pulsing else rl.Color(0, 255, 0, 190) + font = gui_app.font(FontWeight.BOLD) + text_size = 48 + spacing = 0 + + lines = self._alert_text.split('\n') + + # Position text at bottom of alert circle + bottom_y = (alert_rect.y + alert_rect.height) - (alert_rect.height / 7) + + # Draw lines upwards from bottom + current_y = bottom_y - (len(lines) * text_size * FONT_SCALE) + + if self._e2e_alert_display_timer == 0 and ui_state.standstill_timer and self._is_standstill: + # Standstill Timer Text + alert_alt_text = "STOPPED" + top_text_size = 80 + measure_top = measure_text_cached(font, alert_alt_text, top_text_size, spacing) + top_y = alert_rect.y + alert_rect.height / 3.5 + rl.draw_text_ex(font, alert_alt_text, rl.Vector2(center.x - measure_top.x / 2, top_y), top_text_size, spacing, rl.Color(255, 175, 3, 240)) + + # Timer + timer_text_size = 100 + measure_timer = measure_text_cached(font, self._alert_text, timer_text_size, spacing) + timer_y = (alert_rect.y + alert_rect.height) - (alert_rect.height / 5) - measure_timer.y + rl.draw_text_ex(font, self._alert_text, rl.Vector2(center.x - measure_timer.x / 2, timer_y), timer_text_size, spacing, rl.WHITE) + else: + for line in lines: + measure = measure_text_cached(font, line, text_size, spacing) + line_x = center.x - measure.x / 2 + rl.draw_text_ex(font, line, rl.Vector2(line_x, current_y), text_size, spacing, txt_color) + current_y += text_size * FONT_SCALE diff --git a/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py b/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py index 441a169d23..8e19f876a2 100644 --- a/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py +++ b/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py @@ -10,7 +10,7 @@ from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui.elements import ( UiElement, RelDistElement, RelSpeedElement, SteeringAngleElement, DesiredLateralAccelElement, ActualLateralAccelElement, DesiredSteeringAngleElement, AEgoElement, LeadSpeedElement, FrictionCoefficientElement, LatAccelFactorElement, - SteeringTorqueEpsElement, BearingDegElement, AltitudeElement + SteeringTorqueEpsElement, BearingDegElement, AltitudeElement, DesiredSteeringPIDElement ) from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.text_measure import measure_text_cached @@ -19,8 +19,8 @@ from openpilot.system.ui.widgets import Widget class DeveloperUiRenderer(Widget): DEV_UI_OFF = 0 - DEV_UI_RIGHT = 1 - DEV_UI_BOTTOM = 2 + DEV_UI_BOTTOM = 1 + DEV_UI_RIGHT = 2 DEV_UI_BOTH = 3 BOTTOM_BAR_HEIGHT = 61 @@ -36,6 +36,7 @@ class DeveloperUiRenderer(Widget): self.desired_lat_accel_elem = DesiredLateralAccelElement() self.actual_lat_accel_elem = ActualLateralAccelElement() self.desired_steer_elem = DesiredSteeringAngleElement() + self.desired_pid_steer_elem = DesiredSteeringPIDElement() self.a_ego_elem = AEgoElement() self.lead_speed_elem = LeadSpeedElement() self.friction_elem = FrictionCoefficientElement() @@ -61,10 +62,10 @@ class DeveloperUiRenderer(Widget): if sm.recv_frame["carState"] < ui_state.started_frame: return - if self.dev_ui_mode == self.DEV_UI_RIGHT: - self._draw_right_dev_ui(rect) - elif self.dev_ui_mode == self.DEV_UI_BOTTOM: + if self.dev_ui_mode == self.DEV_UI_BOTTOM: self._draw_bottom_dev_ui(rect) + elif self.dev_ui_mode == self.DEV_UI_RIGHT: + self._draw_right_dev_ui(rect) elif self.dev_ui_mode == self.DEV_UI_BOTH: self._draw_right_dev_ui(rect) self._draw_bottom_dev_ui(rect) @@ -85,8 +86,10 @@ class DeveloperUiRenderer(Widget): ] if controls_state.lateralControlState.which() == 'torqueState': elements.append(self.desired_lat_accel_elem.update(sm, ui_state.is_metric)) - else: + elif controls_state.lateralControlState.which() == 'angleState': elements.append(self.desired_steer_elem.update(sm, ui_state.is_metric)) + elif controls_state.lateralControlState.which() == 'pidState': + elements.append(self.desired_pid_steer_elem.update(sm, ui_state.is_metric)) elements.append(self.actual_lat_accel_elem.update(sm, ui_state.is_metric)) diff --git a/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py b/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py index 652469bddd..94e3af42eb 100644 --- a/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py +++ b/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py @@ -191,6 +191,31 @@ class DesiredLateralAccelElement(LateralControlElement): return UiElement(value, "DESIRED L.A.", self.unit, color) +class DesiredSteeringPIDElement(LateralControlElement): + def __init__(self): + self.unit = "" + + def update(self, sm, is_metric: bool) -> UiElement: + car_state = sm['carState'] + controls_state = sm['controlsState'] + lat_active = sm['carControl'].latActive + angle_steers = car_state.steeringAngleDeg + steer_angle_desired = controls_state.lateralControlState.pidState.steeringAngleDesiredDeg + + value = f"{steer_angle_desired:.1f}°" if lat_active else "-" + + color = rl.WHITE + if lat_active: + if abs(angle_steers) > 180: + color = rl.RED + elif abs(angle_steers) > 90: + color = rl.Color(255, 188, 0, 255) + else: + color = rl.Color(0, 255, 0, 255) + + return UiElement(value, "DESIRED STEER", self.unit, color) + + class AEgoElement: def __init__(self): self.unit = "m/s^2" diff --git a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py index 8ca7269802..86fcf3c693 100644 --- a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py +++ b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py @@ -6,13 +6,21 @@ See the LICENSE.md file in the root directory for more details. """ import pyray as rl -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer +from openpilot.common.constants import CV +from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.road_name import RoadNameRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.rocket_fuel import RocketFuel from openpilot.selfdrive.ui.sunnypilot.onroad.speed_limit import SpeedLimitRenderer +from openpilot.selfdrive.ui.sunnypilot.onroad.smart_cruise_control import SmartCruiseControlRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.turn_signal import TurnSignalController +from openpilot.selfdrive.ui.sunnypilot.onroad.circular_alerts import CircularAlertsRenderer +from openpilot.selfdrive.ui.sunnypilot.onroad.speed_renderer import SpeedRenderer +from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus +from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer, UI_CONFIG, FONT_SIZES, COLORS, CRUISE_DISABLED_CHAR +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.lib.text_measure import measure_text_cached class HudRendererSP(HudRenderer): @@ -22,20 +30,109 @@ class HudRendererSP(HudRenderer): self.road_name_renderer = RoadNameRenderer() self.rocket_fuel = RocketFuel() self.speed_limit_renderer = SpeedLimitRenderer() + self.smart_cruise_control_renderer = SmartCruiseControlRenderer() self.turn_signal_controller = TurnSignalController() + self.circular_alerts_renderer = CircularAlertsRenderer() + self.speed_renderer = SpeedRenderer() + self._torque_bar = TorqueBar(scale=3.0, always=True) + + self.pcm_cruise_speed: bool = True + self.show_icbm_status: bool = False + self.icbm_active_counter: int = 0 + self.speed_cluster: float = 0.0 + self.speed_conv: float = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH def _update_state(self) -> None: + if ui_state.sm.recv_frame["carState"] < ui_state.started_frame: + return + + if ui_state.CP_SP is not None: + self.pcm_cruise_speed = ui_state.CP_SP.pcmCruiseSpeed + self.speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH + self.speed_cluster = ui_state.sm['carState'].cruiseState.speedCluster * self.speed_conv + super()._update_state() self.road_name_renderer.update() self.speed_limit_renderer.update() + self.smart_cruise_control_renderer.update() self.turn_signal_controller.update() + self.circular_alerts_renderer.update() + self.speed_renderer.update() + + def _get_icbm_status(self): + if not self.pcm_cruise_speed and ui_state.sm['carControl'].enabled: + if round(self.set_speed) != round(self.speed_cluster): + self.icbm_active_counter = 3 * gui_app.target_fps # 3 seconds usually + elif self.icbm_active_counter > 0: + self.icbm_active_counter -= 1 + else: + self.icbm_active_counter = 0 + + self.show_icbm_status = self.icbm_active_counter > 0 + + def _draw_set_speed(self, rect: rl.Rectangle) -> None: + self._get_icbm_status() + + set_speed_width = UI_CONFIG.set_speed_width_metric if ui_state.is_metric else UI_CONFIG.set_speed_width_imperial + x = rect.x + 60 + (UI_CONFIG.set_speed_width_imperial - set_speed_width) // 2 + y = rect.y + 45 + + set_speed_rect = rl.Rectangle(x, y, set_speed_width, UI_CONFIG.set_speed_height) + rl.draw_rectangle_rounded(set_speed_rect, 0.35, 10, COLORS.BLACK_TRANSLUCENT) + rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 6, COLORS.BORDER_TRANSLUCENT) + + max_color = COLORS.GREY + set_speed_color = COLORS.DARK_GREY + if self.is_cruise_set: + set_speed_color = COLORS.WHITE + if ui_state.status == UIStatus.ENGAGED: + max_color = COLORS.ENGAGED + elif ui_state.status == UIStatus.DISENGAGED: + max_color = COLORS.DISENGAGED + elif ui_state.status == UIStatus.OVERRIDE: + max_color = COLORS.OVERRIDE + + max_str_size = 60 if self.show_icbm_status else 40 + max_str_y = 15 if self.show_icbm_status else 27 + + max_text = str(round(self.speed_cluster)) if self.show_icbm_status else tr("MAX") + max_text_width = measure_text_cached(self._font_semi_bold, max_text, max_str_size).x + rl.draw_text_ex( + self._font_semi_bold, + max_text, + rl.Vector2(x + (set_speed_width - max_text_width) / 2, y + max_str_y), + max_str_size, + 0, + max_color, + ) + + set_speed_text = CRUISE_DISABLED_CHAR if not self.is_cruise_set else str(round(self.set_speed)) + speed_text_width = measure_text_cached(self._font_bold, set_speed_text, FONT_SIZES.set_speed).x + rl.draw_text_ex( + self._font_bold, + set_speed_text, + rl.Vector2(x + (set_speed_width - speed_text_width) / 2, y + 77), + FONT_SIZES.set_speed, + 0, + set_speed_color, + ) + + def _draw_current_speed(self, rect: rl.Rectangle) -> None: + self.speed_renderer.render(rect) def _render(self, rect: rl.Rectangle) -> None: super()._render(rect) + + if ui_state.torque_bar and ui_state.sm['controlsState'].lateralControlState.which() != 'angleState': + torque_rect = rect + if ui_state.developer_ui in (DeveloperUiRenderer.DEV_UI_BOTTOM, DeveloperUiRenderer.DEV_UI_BOTH): + torque_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height - DeveloperUiRenderer.BOTTOM_BAR_HEIGHT) + self._torque_bar.render(torque_rect) + self.developer_ui.render(rect) self.road_name_renderer.render(rect) self.speed_limit_renderer.render(rect) + self.smart_cruise_control_renderer.render(rect) self.turn_signal_controller.render(rect) - - if ui_state.rocket_fuel: - self.rocket_fuel.render(rect, ui_state.sm) + self.circular_alerts_renderer.render(rect) + self.rocket_fuel.render(rect, ui_state.sm) diff --git a/selfdrive/ui/sunnypilot/onroad/road_name.py b/selfdrive/ui/sunnypilot/onroad/road_name.py index 652e620ad6..f85285ef53 100644 --- a/selfdrive/ui/sunnypilot/onroad/road_name.py +++ b/selfdrive/ui/sunnypilot/onroad/road_name.py @@ -31,7 +31,7 @@ class RoadNameRenderer(Widget): self.road_name = lmd.roadName def _render(self, rect: rl.Rectangle): - if not self.road_name: + if not self.road_name or not ui_state.road_name_toggle: return text = self.road_name diff --git a/selfdrive/ui/sunnypilot/onroad/rocket_fuel.py b/selfdrive/ui/sunnypilot/onroad/rocket_fuel.py index af25711a92..cb1012890e 100644 --- a/selfdrive/ui/sunnypilot/onroad/rocket_fuel.py +++ b/selfdrive/ui/sunnypilot/onroad/rocket_fuel.py @@ -6,12 +6,17 @@ See the LICENSE.md file in the root directory for more details. """ import pyray as rl +from openpilot.selfdrive.ui.ui_state import ui_state + class RocketFuel: def __init__(self): self.vc_accel = 0.0 def render(self, rect: rl.Rectangle, sm) -> None: + if not ui_state.rocket_fuel: + return + vc_accel0 = sm['carState'].aEgo # Smooth the acceleration diff --git a/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py b/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py new file mode 100644 index 0000000000..ca71fcac4a --- /dev/null +++ b/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py @@ -0,0 +1,131 @@ +""" +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. +""" +import pyray as rl + +from openpilot.selfdrive.ui.onroad.hud_renderer import COLORS +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets import Widget + + +class SmartCruiseControlRenderer(Widget): + def __init__(self): + super().__init__() + self.vision_enabled = False + self.vision_active = False + self.vision_frame = 0 + self.map_enabled = False + self.map_active = False + self.map_frame = 0 + self.long_override = False + + self.font = gui_app.font(FontWeight.BOLD) + self.scc_tex = rl.load_render_texture(256, 128) + + def update(self): + sm = ui_state.sm + if sm.updated["longitudinalPlanSP"]: + lp_sp = sm["longitudinalPlanSP"] + vision = lp_sp.smartCruiseControl.vision + map_ = lp_sp.smartCruiseControl.map + + self.vision_enabled = vision.enabled + self.vision_active = vision.active + self.map_enabled = map_.enabled + self.map_active = map_.active + + if sm.updated["carControl"]: + self.long_override = sm["carControl"].cruiseControl.override + + if self.vision_active: + self.vision_frame += 1 + else: + self.vision_frame = 0 + + if self.map_active: + self.map_frame += 1 + else: + self.map_frame = 0 + + @staticmethod + def _pulse_element(frame): + return not (frame % gui_app.target_fps < (gui_app.target_fps / 2.5)) + + def _draw_icon(self, rect_center_x, rect_height, x_offset, y_offset, name): + text = name + font_size = 36 + padding_v = 5 + box_width = 160 + + sz = measure_text_cached(self.font, text, font_size) + box_height = int(sz.y + padding_v * 2) + + texture_width = 256 + texture_height = 128 + + rl.begin_texture_mode(self.scc_tex) + rl.clear_background(rl.Color(0, 0, 0, 0)) + + if self.long_override: + box_color = COLORS.OVERRIDE + else: + box_color = rl.Color(0, 255, 0, 255) + + # Center box in texture + box_x = (texture_width - box_width) // 2 + box_y = (texture_height - box_height) // 2 + + rl.draw_rectangle_rounded(rl.Rectangle(box_x, box_y, box_width, box_height), 0.2, 10, box_color) + + # Draw text with custom blend mode to punch hole + rl.rl_set_blend_factors(rl.RL_ZERO, rl.RL_ONE_MINUS_SRC_ALPHA, 0x8006) + rl.rl_set_blend_mode(rl.BLEND_CUSTOM) + + text_pos_x = box_x + (box_width - sz.x) / 2 + text_pos_y = box_y + (box_height - sz.y) / 2 + + rl.draw_text_ex(self.font, text, rl.Vector2(text_pos_x, text_pos_y), font_size, 0, rl.WHITE) + + rl.rl_set_blend_mode(rl.BLEND_ALPHA) # Reset + rl.end_texture_mode() + + screen_y = rect_height / 4 + y_offset + + dest_x = rect_center_x + x_offset - texture_width / 2 + dest_y = screen_y - texture_height / 2 + + src_rect = rl.Rectangle(0, 0, texture_width, -texture_height) + dst_rect = rl.Rectangle(dest_x, dest_y, texture_width, texture_height) + + rl.draw_texture_pro(self.scc_tex.texture, src_rect, dst_rect, rl.Vector2(0, 0), 0, rl.WHITE) + + def _render(self, rect: rl.Rectangle): + x_offset = -260 + y1_offset = -40 + y2_offset = -100 + + orders = [y1_offset, y2_offset] + y_scc_v = 0 + y_scc_m = 0 + idx = 0 + + if self.vision_enabled: + y_scc_v = orders[idx] + idx += 1 + + if self.map_enabled: + y_scc_m = orders[idx] + idx += 1 + + scc_vision_pulse = self._pulse_element(self.vision_frame) + if (self.vision_enabled and not self.vision_active) or (self.vision_active and scc_vision_pulse): + self._draw_icon(rect.x + rect.width / 2, rect.height, x_offset, y_scc_v, "SCC-V") + + scc_map_pulse = self._pulse_element(self.map_frame) + if (self.map_enabled and not self.map_active) or (self.map_active and scc_map_pulse): + self._draw_icon(rect.x + rect.width / 2, rect.height, x_offset, y_scc_m, "SCC-M") diff --git a/selfdrive/ui/sunnypilot/onroad/speed_renderer.py b/selfdrive/ui/sunnypilot/onroad/speed_renderer.py new file mode 100644 index 0000000000..0a017876e1 --- /dev/null +++ b/selfdrive/ui/sunnypilot/onroad/speed_renderer.py @@ -0,0 +1,46 @@ +""" +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. +""" +import pyray as rl + +from openpilot.common.constants import CV +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.selfdrive.ui.onroad.hud_renderer import FONT_SIZES, COLORS + + +class SpeedRenderer: + def __init__(self): + self.speed: float = 0.0 + self.v_ego_cluster_seen: bool = False + + self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD) + self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM) + + def update(self) -> None: + car_state = ui_state.sm['carState'] + v_ego_cluster = car_state.vEgoCluster + self.v_ego_cluster_seen = self.v_ego_cluster_seen or v_ego_cluster != 0.0 + v_ego = v_ego_cluster if self.v_ego_cluster_seen and not ui_state.true_v_ego_ui else car_state.vEgo + speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH + self.speed = max(0.0, v_ego * speed_conversion) + + def render(self, rect: rl.Rectangle) -> None: + if ui_state.hide_v_ego_ui: + return + + # Draw current speed and unit + speed_text = str(round(self.speed)) + speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed) + speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2) + rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.WHITE) + + unit_text = tr("km/h") if ui_state.is_metric else tr("mph") + unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit) + unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2) + rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.WHITE_TRANSLUCENT) diff --git a/selfdrive/ui/sunnypilot/onroad/turn_signal.py b/selfdrive/ui/sunnypilot/onroad/turn_signal.py index 3a66ffeb03..bd1aa7ee10 100644 --- a/selfdrive/ui/sunnypilot/onroad/turn_signal.py +++ b/selfdrive/ui/sunnypilot/onroad/turn_signal.py @@ -72,71 +72,30 @@ class TurnSignalWidget(Widget): class TurnSignalController: - def __init__(self, config: TurnSignalConfig | None = None): - self._config = config or TurnSignalConfig() + def __init__(self): + self._config = TurnSignalConfig() self._left_signal = TurnSignalWidget(direction=IconSide.left) self._right_signal = TurnSignalWidget(direction=IconSide.right) - self._last_icon_side = None + + @staticmethod + def _update_signal(signal, blindspot, blinker): + if ui_state.blindspot and blindspot: + signal.activate('blind_spot') + elif ui_state.turn_signals and blinker: + signal.activate('signal') + else: + signal.deactivate() def update(self): - sm = ui_state.sm - ss = sm['selfdriveState'] + CS = ui_state.sm['carState'] - event_name = ss.alertType.split('/')[0] if ss.alertType else '' - - if event_name == 'preLaneChangeLeft': - self._last_icon_side = IconSide.left - self._left_signal.activate('signal') - self._right_signal.deactivate() - - elif event_name == 'preLaneChangeRight': - self._last_icon_side = IconSide.right - self._right_signal.activate('signal') - self._left_signal.deactivate() - - elif event_name == 'laneChange': - if self._last_icon_side == IconSide.left: - self._left_signal.activate('signal') - self._right_signal.deactivate() - elif self._last_icon_side == IconSide.right: - self._right_signal.activate('signal') - self._left_signal.deactivate() - - elif event_name == 'laneChangeBlocked': - CS = sm['carState'] - if CS.leftBlinker: - icon_side = IconSide.left - elif CS.rightBlinker: - icon_side = IconSide.right - else: - icon_side = self._last_icon_side - - if icon_side == IconSide.left: - self._left_signal.activate('blind_spot') - self._right_signal.deactivate() - elif icon_side == IconSide.right: - self._right_signal.activate('blind_spot') - self._left_signal.deactivate() - - else: - self._last_icon_side = None - CS = sm['carState'] - - if CS.leftBlindspot: - self._left_signal.activate('blind_spot') - elif CS.leftBlinker: - self._left_signal.activate('signal') - else: - self._left_signal.deactivate() - - if CS.rightBlindspot: - self._right_signal.activate('blind_spot') - elif CS.rightBlinker: - self._right_signal.activate('signal') - else: - self._right_signal.deactivate() + self._update_signal(self._left_signal, CS.leftBlindspot, CS.leftBlinker) + self._update_signal(self._right_signal, CS.rightBlindspot, CS.rightBlinker) def render(self, rect: rl.Rectangle): + if not ui_state.turn_signals and not ui_state.blindspot: + return + x = rect.x + rect.width / 2 left_x = x - self._config.left_x - self._config.size diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index f38280d498..4d26e138b1 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -26,6 +26,7 @@ class OnroadTimerStatus(Enum): class UIStateSP: def __init__(self): + self.CP_SP: custom.CarParamsSP | None = None self.params = Params() self.sm_services_ext = [ "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP", @@ -38,6 +39,9 @@ class UIStateSP: self.onroad_brightness_timer: int = 0 self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True) self.reset_onroad_sleep_timer() + self.CP_SP: custom.CarParamsSP | None = None + self.has_icbm: bool = False + self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch") def update(self) -> None: if self.sunnylink_enabled: @@ -120,27 +124,32 @@ class UIStateSP: CP_SP_bytes = self.params.get("CarParamsSPPersistent") if CP_SP_bytes is not None: self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP) - self.sunnylink_enabled = self.params.get_bool("SunnylinkEnabled") - self.developer_ui = self.params.get("DevUIInfo") - self.rocket_fuel = self.params.get_bool("RocketFuel") - self.rainbow_path = self.params.get_bool("RainbowMode") - self.chevron_metrics = self.params.get("ChevronInfo") + self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement") self.active_bundle = self.params.get("ModelManager_ActiveBundle") + self.blindspot = self.params.get_bool("BlindSpot") + self.chevron_metrics = self.params.get("ChevronInfo") self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True) - self.speed_limit_mode = self.params.get("SpeedLimitMode", return_default=True) - - # Onroad Screen Brightness + self.developer_ui = self.params.get("DevUIInfo") + self.hide_v_ego_ui = self.params.get_bool("HideVEgoUI") self.onroad_brightness = int(float(self.params.get("OnroadScreenOffBrightness", return_default=True))) self.onroad_brightness_timer_param = self.params.get("OnroadScreenOffTimer", return_default=True) + self.rainbow_path = self.params.get_bool("RainbowMode") + self.road_name_toggle = self.params.get_bool("RoadNameToggle") + self.rocket_fuel = self.params.get_bool("RocketFuel") + self.speed_limit_mode = self.params.get("SpeedLimitMode", return_default=True) + self.standstill_timer = self.params.get_bool("StandstillTimer") + self.sunnylink_enabled = self.params.get_bool("SunnylinkEnabled") + self.torque_bar = self.params.get_bool("TorqueBar") + self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI") + self.turn_signals = self.params.get_bool("ShowTurnSignals") + self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True) class DeviceSP: - def __init__(self): - self._params = Params() - - def _set_awake(self, on: bool): - if on and self._params.get("DeviceBootMode", return_default=True) == 1: - self._params.put_bool("OffroadMode", True) + @staticmethod + def _set_awake(on: bool, _ui_state): + if _ui_state.boot_offroad_mode == 1 and not on: + _ui_state.params.put_bool("OffroadMode", True) @staticmethod def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float: diff --git a/selfdrive/ui/tests/diff/diff.py b/selfdrive/ui/tests/diff/diff.py index be7af5438a..a581f68747 100755 --- a/selfdrive/ui/tests/diff/diff.py +++ b/selfdrive/ui/tests/diff/diff.py @@ -3,7 +3,6 @@ import os import sys import subprocess import tempfile -import base64 import webbrowser import argparse from pathlib import Path @@ -25,12 +24,6 @@ def compare_frames(frame1_path, frame2_path): return result.returncode == 0 -def frame_to_data_url(frame_path): - with open(frame_path, 'rb') as f: - data = f.read() - return f"data:image/png;base64,{base64.b64encode(data).decode()}" - - def create_diff_video(video1, video2, output_path): """Create a diff video using ffmpeg blend filter with difference mode.""" print("Creating diff video...") @@ -60,20 +53,16 @@ def find_differences(video1, video2): print(f"Comparing {len(frames1)} frames...") different_frames = [] - frame_data = [] for i, (f1, f2) in enumerate(zip(frames1, frames2, strict=False)): is_different = not compare_frames(f1, f2) if is_different: different_frames.append(i) - if i < 10 or i >= len(frames1) - 10 or is_different: - frame_data.append({'index': i, 'different': is_different, 'frame1_url': frame_to_data_url(f1), 'frame2_url': frame_to_data_url(f2)}) - - return different_frames, frame_data, len(frames1) + return different_frames, len(frames1) -def generate_html_report(video1, video2, basedir, different_frames, frame_data, total_frames): +def generate_html_report(video1, video2, basedir, different_frames, total_frames): chunks = [] if different_frames: current_chunk = [different_frames[0]] @@ -177,14 +166,14 @@ def main(): diff_video_path = os.path.join(os.path.dirname(args.output), DIFF_OUT_DIR / "diff.mp4") create_diff_video(args.video1, args.video2, diff_video_path) - different_frames, frame_data, total_frames = find_differences(args.video1, args.video2) + different_frames, total_frames = find_differences(args.video1, args.video2) if different_frames is None: sys.exit(1) print() print("Generating HTML report...") - html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, frame_data, total_frames) + html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, total_frames) with open(DIFF_OUT_DIR / args.output, 'w') as f: f.write(html) diff --git a/selfdrive/ui/translations/app_ar.po b/selfdrive/ui/translations/app_ar.po deleted file mode 100644 index 608389fc07..0000000000 --- a/selfdrive/ui/translations/app_ar.po +++ /dev/null @@ -1,1218 +0,0 @@ -# Arabic translations for PACKAGE package. -# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# Automatically generated, 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-10-23 00:50-0700\n" -"PO-Revision-Date: 2025-10-22 16:32-0700\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: ar\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=6; plural=n==0?0:n==1?1:n==2?2:(n%100>=3 && " -"n%100<=10)?3:(n%100>=11 && n%100<=99)?4:5;\n" - -#: selfdrive/ui/layouts/settings/device.py:160 -#, python-format -msgid " Steering torque response calibration is complete." -msgstr " اكتملت معايرة استجابة عزم التوجيه." - -#: selfdrive/ui/layouts/settings/device.py:158 -#, python-format -msgid " Steering torque response calibration is {}% complete." -msgstr " اكتملت معايرة استجابة عزم التوجيه بنسبة {}٪." - -#: selfdrive/ui/layouts/settings/device.py:133 -#, python-format -msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." -msgstr " جهازك موجه بمقدار {:.1f}° {} و {:.1f}° {}." - -#: selfdrive/ui/layouts/sidebar.py:43 -msgid "--" -msgstr "--" - -#: selfdrive/ui/widgets/prime.py:47 -#, python-format -msgid "1 year of drive storage" -msgstr "سنة واحدة من تخزين القيادة" - -#: selfdrive/ui/widgets/prime.py:47 -#, python-format -msgid "24/7 LTE connectivity" -msgstr "اتصال LTE على مدار الساعة" - -#: selfdrive/ui/layouts/sidebar.py:46 -msgid "2G" -msgstr "2G" - -#: selfdrive/ui/layouts/sidebar.py:47 -msgid "3G" -msgstr "3G" - -#: selfdrive/ui/layouts/sidebar.py:49 -msgid "5G" -msgstr "5G" - -#: selfdrive/ui/layouts/settings/developer.py:23 -msgid "" -"WARNING: openpilot longitudinal control is in alpha for this car and will " -"disable Automatic Emergency Braking (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. Changing this setting will restart openpilot if the car is " -"powered on." -msgstr "" -"تحذير: التحكم الطولي لـ openpilot في مرحلة ألفا لهذه السيارة وسيُعطّل نظام " -"الكبح التلقائي في حالات الطوارئ (AEB).

في هذه السيارة، يعتمد " -"openpilot افتراضياً على نظام ACC المدمج بدلاً من التحكم الطولي لـ openpilot. " -"فعّل هذا الخيار للتبديل إلى التحكم الطولي لـ openpilot. يُنصح بتمكين وضع " -"التجربة عند تفعيل نسخة ألفا من التحكم الطولي. تغيير هذا الإعداد سيعيد تشغيل " -"openpilot إذا كانت السيارة قيد التشغيل." - -#: selfdrive/ui/layouts/settings/device.py:148 -#, python-format -msgid "

Steering lag calibration is complete." -msgstr "

اكتملت معايرة تأخر التوجيه." - -#: selfdrive/ui/layouts/settings/device.py:146 -#, python-format -msgid "

Steering lag calibration is {}% complete." -msgstr "

اكتملت معايرة تأخر التوجيه بنسبة {}٪." - -#: selfdrive/ui/layouts/settings/firehose.py:138 -#, python-format -msgid "ACTIVE" -msgstr "نشط" - -#: selfdrive/ui/layouts/settings/developer.py:15 -msgid "" -"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." -msgstr "" -"يتيح ADB (Android Debug Bridge) الاتصال بجهازك عبر USB أو عبر الشبكة. راجع " -"https://docs.comma.ai/how-to/connect-to-comma لمزيد من المعلومات." - -#: selfdrive/ui/widgets/ssh_key.py:30 -msgid "ADD" -msgstr "إضافة" - -#: system/ui/widgets/network.py:139 -#, python-format -msgid "APN Setting" -msgstr "إعداد APN" - -#: selfdrive/ui/widgets/offroad_alerts.py:109 -#, python-format -msgid "Acknowledge Excessive Actuation" -msgstr "تأكيد التشغيل المفرط" - -#: system/ui/widgets/network.py:74 system/ui/widgets/network.py:95 -#, python-format -msgid "Advanced" -msgstr "متقدم" - -#: selfdrive/ui/layouts/settings/toggles.py:98 -#, python-format -msgid "Aggressive" -msgstr "عدواني" - -#: selfdrive/ui/layouts/onboarding.py:116 -#, python-format -msgid "Agree" -msgstr "موافقة" - -#: selfdrive/ui/layouts/settings/toggles.py:70 -#, python-format -msgid "Always-On Driver Monitoring" -msgstr "مراقبة السائق دائماً" - -#: selfdrive/ui/layouts/settings/toggles.py:186 -#, python-format -msgid "" -"An alpha version of openpilot longitudinal control can be tested, along with " -"Experimental mode, on non-release branches." -msgstr "" -"يمكن اختبار نسخة ألفا من التحكم الطولي لـ openpilot، مع وضع التجربة، على " -"الفروع غير الإصدارية." - -#: selfdrive/ui/layouts/settings/device.py:187 -#, python-format -msgid "Are you sure you want to power off?" -msgstr "هل أنت متأكد أنك تريد إيقاف التشغيل؟" - -#: selfdrive/ui/layouts/settings/device.py:175 -#, python-format -msgid "Are you sure you want to reboot?" -msgstr "هل أنت متأكد أنك تريد إعادة التشغيل؟" - -#: selfdrive/ui/layouts/settings/device.py:119 -#, python-format -msgid "Are you sure you want to reset calibration?" -msgstr "هل أنت متأكد أنك تريد إعادة ضبط المعايرة؟" - -#: selfdrive/ui/layouts/settings/software.py:163 -#, python-format -msgid "Are you sure you want to uninstall?" -msgstr "هل أنت متأكد أنك تريد إلغاء التثبيت؟" - -#: system/ui/widgets/network.py:99 selfdrive/ui/layouts/onboarding.py:147 -#, python-format -msgid "Back" -msgstr "رجوع" - -#: selfdrive/ui/widgets/prime.py:38 -#, python-format -msgid "Become a comma prime member at connect.comma.ai" -msgstr "انضم إلى comma prime عبر connect.comma.ai" - -#: selfdrive/ui/widgets/pairing_dialog.py:130 -#, python-format -msgid "Bookmark connect.comma.ai to your home screen to use it like an app" -msgstr "ثبّت connect.comma.ai على شاشتك الرئيسية لاستخدامه كتطبيق" - -#: selfdrive/ui/layouts/settings/device.py:68 -#, python-format -msgid "CHANGE" -msgstr "تغيير" - -#: selfdrive/ui/layouts/settings/software.py:50 -#: selfdrive/ui/layouts/settings/software.py:107 -#: selfdrive/ui/layouts/settings/software.py:118 -#: selfdrive/ui/layouts/settings/software.py:147 -#, python-format -msgid "CHECK" -msgstr "تحقق" - -#: selfdrive/ui/widgets/exp_mode_button.py:50 -#, python-format -msgid "CHILL MODE ON" -msgstr "وضع الهدوء مُفعل" - -#: system/ui/widgets/network.py:155 selfdrive/ui/layouts/sidebar.py:73 -#: selfdrive/ui/layouts/sidebar.py:134 selfdrive/ui/layouts/sidebar.py:136 -#: selfdrive/ui/layouts/sidebar.py:138 -#, python-format -msgid "CONNECT" -msgstr "CONNECT" - -#: system/ui/widgets/network.py:369 -#, python-format -msgid "CONNECTING..." -msgstr "CONNECTING..." - -#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 -#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 -#, python-format -msgid "Cancel" -msgstr "إلغاء" - -#: system/ui/widgets/network.py:134 -#, python-format -msgid "Cellular Metered" -msgstr "خلوي بتعرفة محدودة" - -#: selfdrive/ui/layouts/settings/device.py:68 -#, python-format -msgid "Change Language" -msgstr "تغيير اللغة" - -#: selfdrive/ui/layouts/settings/toggles.py:125 -#, python-format -msgid "Changing this setting will restart openpilot if the car is powered on." -msgstr "" -"سيؤدي تغيير هذا الإعداد إلى إعادة تشغيل openpilot إذا كانت السيارة قيد " -"التشغيل." - -#: selfdrive/ui/widgets/pairing_dialog.py:129 -#, python-format -msgid "Click \"add new device\" and scan the QR code on the right" -msgstr "اضغط \"إضافة جهاز جديد\" ثم امسح رمز QR على اليمين" - -#: selfdrive/ui/widgets/offroad_alerts.py:104 -#, python-format -msgid "Close" -msgstr "إغلاق" - -#: selfdrive/ui/layouts/settings/software.py:49 -#, python-format -msgid "Current Version" -msgstr "الإصدار الحالي" - -#: selfdrive/ui/layouts/settings/software.py:110 -#, python-format -msgid "DOWNLOAD" -msgstr "تنزيل" - -#: selfdrive/ui/layouts/onboarding.py:115 -#, python-format -msgid "Decline" -msgstr "رفض" - -#: selfdrive/ui/layouts/onboarding.py:148 -#, python-format -msgid "Decline, uninstall openpilot" -msgstr "رفض، وإلغاء تثبيت openpilot" - -#: selfdrive/ui/layouts/settings/settings.py:67 -msgid "Developer" -msgstr "المطور" - -#: selfdrive/ui/layouts/settings/settings.py:62 -msgid "Device" -msgstr "الجهاز" - -#: selfdrive/ui/layouts/settings/toggles.py:58 -#, python-format -msgid "Disengage on Accelerator Pedal" -msgstr "فصل عند الضغط على دواسة الوقود" - -#: selfdrive/ui/layouts/settings/device.py:184 -#, python-format -msgid "Disengage to Power Off" -msgstr "افصل لإيقاف التشغيل" - -#: selfdrive/ui/layouts/settings/device.py:172 -#, python-format -msgid "Disengage to Reboot" -msgstr "افصل لإعادة التشغيل" - -#: selfdrive/ui/layouts/settings/device.py:103 -#, python-format -msgid "Disengage to Reset Calibration" -msgstr "افصل لإعادة ضبط المعايرة" - -#: selfdrive/ui/layouts/settings/toggles.py:32 -msgid "Display speed in km/h instead of mph." -msgstr "عرض السرعة بالكيلومتر/ساعة بدلاً من الميل/ساعة." - -#: selfdrive/ui/layouts/settings/device.py:59 -#, python-format -msgid "Dongle ID" -msgstr "معرّف الدونجل" - -#: selfdrive/ui/layouts/settings/software.py:50 -#, python-format -msgid "Download" -msgstr "تنزيل" - -#: selfdrive/ui/layouts/settings/device.py:62 -#, python-format -msgid "Driver Camera" -msgstr "كاميرا السائق" - -#: selfdrive/ui/layouts/settings/toggles.py:96 -#, python-format -msgid "Driving Personality" -msgstr "شخصية القيادة" - -#: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 -#, python-format -msgid "EDIT" -msgstr "تعديل" - -#: selfdrive/ui/layouts/sidebar.py:138 -msgid "ERROR" -msgstr "خطأ" - -#: selfdrive/ui/layouts/sidebar.py:45 -msgid "ETH" -msgstr "ETH" - -#: selfdrive/ui/widgets/exp_mode_button.py:50 -#, python-format -msgid "EXPERIMENTAL MODE ON" -msgstr "وضع التجربة مُفعل" - -#: selfdrive/ui/layouts/settings/developer.py:166 -#: selfdrive/ui/layouts/settings/toggles.py:228 -#, python-format -msgid "Enable" -msgstr "تمكين" - -#: selfdrive/ui/layouts/settings/developer.py:39 -#, python-format -msgid "Enable ADB" -msgstr "تمكين ADB" - -#: selfdrive/ui/layouts/settings/toggles.py:64 -#, python-format -msgid "Enable Lane Departure Warnings" -msgstr "تمكين تحذيرات مغادرة المسار" - -#: system/ui/widgets/network.py:129 -#, python-format -msgid "Enable Roaming" -msgstr "تمكين التجوال" - -#: selfdrive/ui/layouts/settings/developer.py:48 -#, python-format -msgid "Enable SSH" -msgstr "تمكين SSH" - -#: system/ui/widgets/network.py:120 -#, python-format -msgid "Enable Tethering" -msgstr "تمكين الربط" - -#: selfdrive/ui/layouts/settings/toggles.py:30 -msgid "Enable driver monitoring even when openpilot is not engaged." -msgstr "تمكين مراقبة السائق حتى عندما لا يكون openpilot مُشغلاً." - -#: selfdrive/ui/layouts/settings/toggles.py:46 -#, python-format -msgid "Enable openpilot" -msgstr "تمكين openpilot" - -#: selfdrive/ui/layouts/settings/toggles.py:189 -#, python-format -msgid "" -"Enable the openpilot longitudinal control (alpha) toggle to allow " -"Experimental mode." -msgstr "فعّل تبديل التحكم الطولي (ألفا) لـ openpilot للسماح بوضع التجربة." - -#: system/ui/widgets/network.py:204 -#, python-format -msgid "Enter APN" -msgstr "أدخل APN" - -#: system/ui/widgets/network.py:241 -#, python-format -msgid "Enter SSID" -msgstr "أدخل SSID" - -#: system/ui/widgets/network.py:254 -#, python-format -msgid "Enter new tethering password" -msgstr "أدخل كلمة مرور الربط الجديدة" - -#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 -#, python-format -msgid "Enter password" -msgstr "أدخل كلمة المرور" - -#: selfdrive/ui/widgets/ssh_key.py:89 -#, python-format -msgid "Enter your GitHub username" -msgstr "أدخل اسم مستخدم GitHub الخاص بك" - -#: system/ui/widgets/list_view.py:123 system/ui/widgets/list_view.py:160 -#, python-format -msgid "Error" -msgstr "خطأ" - -#: selfdrive/ui/layouts/settings/toggles.py:52 -#, python-format -msgid "Experimental Mode" -msgstr "وضع التجربة" - -#: selfdrive/ui/layouts/settings/toggles.py:181 -#, python-format -msgid "" -"Experimental mode is currently unavailable on this car since the car's stock " -"ACC is used for longitudinal control." -msgstr "" -"وضع التجربة غير متاح حالياً في هذه السيارة لأن نظام ACC الأصلي يُستخدم للتحكم " -"الطولي." - -#: system/ui/widgets/network.py:373 -#, python-format -msgid "FORGETTING..." -msgstr "جارٍ النسيان..." - -#: selfdrive/ui/widgets/setup.py:44 -#, python-format -msgid "Finish Setup" -msgstr "إنهاء الإعداد" - -#: selfdrive/ui/layouts/settings/settings.py:66 -msgid "Firehose" -msgstr "Firehose" - -#: selfdrive/ui/layouts/settings/firehose.py:18 -msgid "Firehose Mode" -msgstr "وضع Firehose" - -#: selfdrive/ui/layouts/settings/firehose.py:25 -msgid "" -"For maximum effectiveness, bring your device inside and connect to a good " -"USB-C adapter and Wi-Fi weekly.\n" -"\n" -"Firehose Mode can also work while you're driving if connected to a hotspot " -"or unlimited SIM card.\n" -"\n" -"\n" -"Frequently Asked Questions\n" -"\n" -"Does it matter how or where I drive? Nope, just drive as you normally " -"would.\n" -"\n" -"Do all of my segments get pulled in Firehose Mode? No, we selectively pull a " -"subset of your segments.\n" -"\n" -"What's a good USB-C adapter? Any fast phone or laptop charger should be " -"fine.\n" -"\n" -"Does it matter which software I run? Yes, only upstream openpilot (and " -"particular forks) are able to be used for training." -msgstr "" -"لأقصى فعالية، أحضر جهازك إلى الداخل واتصل بمحوّل USB‑C جيد وبشبكة Wi‑Fi " -"أسبوعياً.\n" -"\n" -"يمكن أن يعمل وضع Firehose أيضاً أثناء القيادة إذا كنت متصلاً بنقطة اتصال أو " -"بشريحة غير محدودة.\n" -"\n" -"\n" -"الأسئلة الشائعة\n" -"\n" -"هل يهم كيف أو أين أقود؟ لا، قد بقدر المعتاد.\n" -"\n" -"هل يتم سحب كل مقاطعي في وضع Firehose؟ لا، نقوم بسحب مجموعة فرعية من " -"المقاطع.\n" -"\n" -"ما هو محول USB‑C الجيد؟ أي شاحن هاتف أو حاسب محمول سريع سيكون مناسباً.\n" -"\n" -"هل يهم أي برنامج أشغّل؟ نعم، فقط openpilot الأصلي (وبعض التفرعات المحددة) " -"يمكن استخدامه للتدريب." - -#: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 -#, python-format -msgid "Forget" -msgstr "نسيان" - -#: system/ui/widgets/network.py:319 -#, python-format -msgid "Forget Wi-Fi Network \"{}\"?" -msgstr "هل تريد نسيان شبكة Wi‑Fi \"{}\"؟" - -#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 -msgid "GOOD" -msgstr "جيد" - -#: selfdrive/ui/widgets/pairing_dialog.py:128 -#, python-format -msgid "Go to https://connect.comma.ai on your phone" -msgstr "اذهب إلى https://connect.comma.ai على هاتفك" - -#: selfdrive/ui/layouts/sidebar.py:129 -msgid "HIGH" -msgstr "مرتفع" - -#: system/ui/widgets/network.py:155 -#, python-format -msgid "Hidden Network" -msgstr "شبكة مخفية" - -#: selfdrive/ui/layouts/settings/firehose.py:140 -#, python-format -msgid "INACTIVE: connect to an unmetered network" -msgstr "غير نشط: اتصل بشبكة غير محدودة التعرفة" - -#: selfdrive/ui/layouts/settings/software.py:53 -#: selfdrive/ui/layouts/settings/software.py:136 -#, python-format -msgid "INSTALL" -msgstr "تثبيت" - -#: system/ui/widgets/network.py:150 -#, python-format -msgid "IP Address" -msgstr "عنوان IP" - -#: selfdrive/ui/layouts/settings/software.py:53 -#, python-format -msgid "Install Update" -msgstr "تثبيت التحديث" - -#: selfdrive/ui/layouts/settings/developer.py:56 -#, python-format -msgid "Joystick Debug Mode" -msgstr "وضع تصحيح عصا التحكم" - -#: selfdrive/ui/widgets/ssh_key.py:29 -msgid "LOADING" -msgstr "جارٍ التحميل" - -#: selfdrive/ui/layouts/sidebar.py:48 -msgid "LTE" -msgstr "LTE" - -#: selfdrive/ui/layouts/settings/developer.py:64 -#, python-format -msgid "Longitudinal Maneuver Mode" -msgstr "وضع المناورة الطولية" - -#: selfdrive/ui/onroad/hud_renderer.py:148 -#, python-format -msgid "MAX" -msgstr "أقصى" - -#: selfdrive/ui/widgets/setup.py:75 -#, python-format -msgid "" -"Maximize your training data uploads to improve openpilot's driving models." -msgstr "زد من تحميل بيانات التدريب لتحسين نماذج قيادة openpilot." - -#: selfdrive/ui/layouts/settings/device.py:59 -#: selfdrive/ui/layouts/settings/device.py:60 -#, python-format -msgid "N/A" -msgstr "غير متوفر" - -#: selfdrive/ui/layouts/sidebar.py:142 -msgid "NO" -msgstr "لا" - -#: selfdrive/ui/layouts/settings/settings.py:63 -msgid "Network" -msgstr "الشبكة" - -#: selfdrive/ui/widgets/ssh_key.py:114 -#, python-format -msgid "No SSH keys found" -msgstr "لم يتم العثور على مفاتيح SSH" - -#: selfdrive/ui/widgets/ssh_key.py:126 -#, python-format -msgid "No SSH keys found for user '{}'" -msgstr "لم يتم العثور على مفاتيح SSH للمستخدم '{}'" - -#: selfdrive/ui/widgets/offroad_alerts.py:320 -#, python-format -msgid "No release notes available." -msgstr "لا توجد ملاحظات إصدار متاحة." - -#: selfdrive/ui/layouts/sidebar.py:73 selfdrive/ui/layouts/sidebar.py:134 -msgid "OFFLINE" -msgstr "غير متصل" - -#: system/ui/widgets/html_render.py:263 system/ui/widgets/confirm_dialog.py:93 -#: selfdrive/ui/layouts/sidebar.py:127 -#, python-format -msgid "OK" -msgstr "موافق" - -#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:136 -#: selfdrive/ui/layouts/sidebar.py:144 -msgid "ONLINE" -msgstr "متصل" - -#: selfdrive/ui/widgets/setup.py:20 -#, python-format -msgid "Open" -msgstr "فتح" - -#: selfdrive/ui/layouts/settings/device.py:48 -#, python-format -msgid "PAIR" -msgstr "إقران" - -#: selfdrive/ui/layouts/sidebar.py:142 -msgid "PANDA" -msgstr "PANDA" - -#: selfdrive/ui/layouts/settings/device.py:62 -#, python-format -msgid "PREVIEW" -msgstr "معاينة" - -#: selfdrive/ui/widgets/prime.py:44 -#, python-format -msgid "PRIME FEATURES:" -msgstr "ميزات PRIME:" - -#: selfdrive/ui/layouts/settings/device.py:48 -#, python-format -msgid "Pair Device" -msgstr "إقران الجهاز" - -#: selfdrive/ui/widgets/setup.py:19 -#, python-format -msgid "Pair device" -msgstr "إقران الجهاز" - -#: selfdrive/ui/widgets/pairing_dialog.py:103 -#, python-format -msgid "Pair your device to your comma account" -msgstr "قم بإقران جهازك بحساب comma" - -#: selfdrive/ui/widgets/setup.py:48 selfdrive/ui/layouts/settings/device.py:24 -#, python-format -msgid "" -"Pair your device with comma connect (connect.comma.ai) and claim your comma " -"prime offer." -msgstr "" -"أقرِن جهازك مع comma connect (connect.comma.ai) واحصل على عرض comma prime." - -#: selfdrive/ui/widgets/setup.py:91 -#, python-format -msgid "Please connect to Wi-Fi to complete initial pairing" -msgstr "يرجى الاتصال بشبكة Wi‑Fi لإكمال الاقتران الأولي" - -#: selfdrive/ui/layouts/settings/device.py:55 -#: selfdrive/ui/layouts/settings/device.py:187 -#, python-format -msgid "Power Off" -msgstr "إيقاف التشغيل" - -#: system/ui/widgets/network.py:144 -#, python-format -msgid "Prevent large data uploads when on a metered Wi-Fi connection" -msgstr "منع رفع البيانات الكبيرة عند الاتصال بشبكة Wi‑Fi محدودة التعرفة" - -#: system/ui/widgets/network.py:135 -#, python-format -msgid "Prevent large data uploads when on a metered cellular connection" -msgstr "منع رفع البيانات الكبيرة عند الاتصال الخلوي محدود التعرفة" - -#: selfdrive/ui/layouts/settings/device.py:25 -msgid "" -"Preview the driver facing camera to ensure that driver monitoring has good " -"visibility. (vehicle must be off)" -msgstr "" -"عاين كاميرا مواجهة السائق للتأكد من أن مراقبة السائق تتم برؤية جيدة. (يجب أن " -"تكون المركبة متوقفة)" - -#: selfdrive/ui/widgets/pairing_dialog.py:161 -#, python-format -msgid "QR Code Error" -msgstr "خطأ في رمز QR" - -#: selfdrive/ui/widgets/ssh_key.py:31 -msgid "REMOVE" -msgstr "إزالة" - -#: selfdrive/ui/layouts/settings/device.py:51 -#, python-format -msgid "RESET" -msgstr "إعادة ضبط" - -#: selfdrive/ui/layouts/settings/device.py:65 -#, python-format -msgid "REVIEW" -msgstr "مراجعة" - -#: selfdrive/ui/layouts/settings/device.py:55 -#: selfdrive/ui/layouts/settings/device.py:175 -#, python-format -msgid "Reboot" -msgstr "إعادة التشغيل" - -#: selfdrive/ui/onroad/alert_renderer.py:66 -#, python-format -msgid "Reboot Device" -msgstr "إعادة تشغيل الجهاز" - -#: selfdrive/ui/widgets/offroad_alerts.py:112 -#, python-format -msgid "Reboot and Update" -msgstr "إعادة التشغيل والتحديث" - -#: selfdrive/ui/layouts/settings/toggles.py:27 -msgid "" -"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)." -msgstr "" -"استقبال تنبيهات للتوجيه للعودة إلى المسار عند انحراف المركبة فوق خط المسار " -"المُكتشف بدون إشارة انعطاف مفعّلة أثناء القيادة فوق 31 ميل/س (50 كم/س)." - -#: selfdrive/ui/layouts/settings/toggles.py:76 -#, python-format -msgid "Record and Upload Driver Camera" -msgstr "تسجيل ورفع فيديو كاميرا السائق" - -#: selfdrive/ui/layouts/settings/toggles.py:82 -#, python-format -msgid "Record and Upload Microphone Audio" -msgstr "تسجيل ورفع صوت الميكروفون" - -#: selfdrive/ui/layouts/settings/toggles.py:33 -msgid "" -"Record and store microphone audio while driving. The audio will be included " -"in the dashcam video in comma connect." -msgstr "" -"تسجيل وتخزين صوت الميكروفون أثناء القيادة. سيُدرج الصوت في فيديو الكاميرا " -"الأمامية في comma connect." - -#: selfdrive/ui/layouts/settings/device.py:67 -#, python-format -msgid "Regulatory" -msgstr "لوائح" - -#: selfdrive/ui/layouts/settings/toggles.py:98 -#, python-format -msgid "Relaxed" -msgstr "مسترخٍ" - -#: selfdrive/ui/widgets/prime.py:47 -#, python-format -msgid "Remote access" -msgstr "وصول عن بُعد" - -#: selfdrive/ui/widgets/prime.py:47 -#, python-format -msgid "Remote snapshots" -msgstr "لقطات عن بُعد" - -#: selfdrive/ui/widgets/ssh_key.py:123 -#, python-format -msgid "Request timed out" -msgstr "انتهت مهلة الطلب" - -#: selfdrive/ui/layouts/settings/device.py:119 -#, python-format -msgid "Reset" -msgstr "إعادة ضبط" - -#: selfdrive/ui/layouts/settings/device.py:51 -#, python-format -msgid "Reset Calibration" -msgstr "إعادة ضبط المعايرة" - -#: selfdrive/ui/layouts/settings/device.py:65 -#, python-format -msgid "Review Training Guide" -msgstr "مراجعة دليل التدريب" - -#: selfdrive/ui/layouts/settings/device.py:27 -msgid "Review the rules, features, and limitations of openpilot" -msgstr "مراجعة قواعد وميزات وحدود openpilot" - -#: selfdrive/ui/layouts/settings/software.py:61 -#, python-format -msgid "SELECT" -msgstr "اختيار" - -#: selfdrive/ui/layouts/settings/developer.py:53 -#, python-format -msgid "SSH Keys" -msgstr "مفاتيح SSH" - -#: system/ui/widgets/network.py:310 -#, python-format -msgid "Scanning Wi-Fi networks..." -msgstr "جارٍ مسح شبكات Wi‑Fi..." - -#: system/ui/widgets/option_dialog.py:36 -#, python-format -msgid "Select" -msgstr "اختيار" - -#: selfdrive/ui/layouts/settings/software.py:183 -#, python-format -msgid "Select a branch" -msgstr "اختر فرعاً" - -#: selfdrive/ui/layouts/settings/device.py:91 -#, python-format -msgid "Select a language" -msgstr "اختر لغة" - -#: selfdrive/ui/layouts/settings/device.py:60 -#, python-format -msgid "Serial" -msgstr "الرقم التسلسلي" - -#: selfdrive/ui/widgets/offroad_alerts.py:106 -#, python-format -msgid "Snooze Update" -msgstr "تأجيل التحديث" - -#: selfdrive/ui/layouts/settings/settings.py:65 -msgid "Software" -msgstr "البرمجيات" - -#: selfdrive/ui/layouts/settings/toggles.py:98 -#, python-format -msgid "Standard" -msgstr "قياسي" - -#: selfdrive/ui/layouts/settings/toggles.py:22 -msgid "" -"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." -msgstr "" -"يوصى بالوضع القياسي. في الوضع العدواني، سيتبع openpilot السيارات الأمامية عن " -"قرب وسيكون أكثر شدة في الوقود والفرامل. في الوضع المسترخي سيبقى بعيداً أكثر " -"عن السيارات الأمامية. في السيارات المدعومة، يمكنك التنقل بين هذه الشخصيات " -"بزر مسافة المقود." - -#: selfdrive/ui/onroad/alert_renderer.py:59 -#: selfdrive/ui/onroad/alert_renderer.py:65 -#, python-format -msgid "System Unresponsive" -msgstr "النظام لا يستجيب" - -#: selfdrive/ui/onroad/alert_renderer.py:58 -#, python-format -msgid "TAKE CONTROL IMMEDIATELY" -msgstr "تولَّ السيطرة فوراً" - -#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 -#: selfdrive/ui/layouts/sidebar.py:127 selfdrive/ui/layouts/sidebar.py:129 -msgid "TEMP" -msgstr "الحرارة" - -#: selfdrive/ui/layouts/settings/software.py:61 -#, python-format -msgid "Target Branch" -msgstr "الفرع المستهدف" - -#: system/ui/widgets/network.py:124 -#, python-format -msgid "Tethering Password" -msgstr "كلمة مرور الربط" - -#: selfdrive/ui/layouts/settings/settings.py:64 -msgid "Toggles" -msgstr "مفاتيح التبديل" - -#: selfdrive/ui/layouts/settings/software.py:72 -#, python-format -msgid "UNINSTALL" -msgstr "إلغاء التثبيت" - -#: selfdrive/ui/layouts/home.py:155 -#, python-format -msgid "UPDATE" -msgstr "تحديث" - -#: selfdrive/ui/layouts/settings/software.py:72 -#: selfdrive/ui/layouts/settings/software.py:163 -#, python-format -msgid "Uninstall" -msgstr "إلغاء التثبيت" - -#: selfdrive/ui/layouts/sidebar.py:117 -msgid "Unknown" -msgstr "غير معروف" - -#: selfdrive/ui/layouts/settings/software.py:48 -#, python-format -msgid "Updates are only downloaded while the car is off." -msgstr "يتم تنزيل التحديثات فقط عندما تكون السيارة متوقفة." - -#: selfdrive/ui/widgets/prime.py:33 -#, python-format -msgid "Upgrade Now" -msgstr "الترقية الآن" - -#: selfdrive/ui/layouts/settings/toggles.py:31 -msgid "" -"Upload data from the driver facing camera and help improve the driver " -"monitoring algorithm." -msgstr "" -"ارفع بيانات من كاميرا مواجهة السائق وساعد في تحسين خوارزمية مراقبة السائق." - -#: selfdrive/ui/layouts/settings/toggles.py:88 -#, python-format -msgid "Use Metric System" -msgstr "استخدام النظام المتري" - -#: selfdrive/ui/layouts/settings/toggles.py:17 -msgid "" -"Use the openpilot system for adaptive cruise control and lane keep driver " -"assistance. Your attention is required at all times to use this feature." -msgstr "" -"استخدم نظام openpilot للتحكم الذكي بالسرعة والمساعدة على البقاء داخل المسار. " -"يتطلب استخدام هذه الميزة انتباهك الكامل في جميع الأوقات." - -#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:144 -msgid "VEHICLE" -msgstr "المركبة" - -#: selfdrive/ui/layouts/settings/device.py:67 -#, python-format -msgid "VIEW" -msgstr "عرض" - -#: selfdrive/ui/onroad/alert_renderer.py:52 -#, python-format -msgid "Waiting to start" -msgstr "بانتظار البدء" - -#: selfdrive/ui/layouts/settings/developer.py:19 -msgid "" -"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." -msgstr "" -"تحذير: يمنح هذا وصول SSH إلى جميع المفاتيح العامة في إعدادات GitHub الخاصة " -"بك. لا تُدخل مطلقاً اسم مستخدم GitHub غير اسمك. لن يطلب منك موظف في comma أبداً " -"إضافة اسم مستخدمهم." - -#: selfdrive/ui/layouts/onboarding.py:111 -#, python-format -msgid "Welcome to openpilot" -msgstr "مرحباً بك في openpilot" - -#: selfdrive/ui/layouts/settings/toggles.py:20 -msgid "When enabled, pressing the accelerator pedal will disengage openpilot." -msgstr "عند التمكين، سيؤدي الضغط على دواسة الوقود إلى فصل openpilot." - -#: selfdrive/ui/layouts/sidebar.py:44 -msgid "Wi-Fi" -msgstr "Wi‑Fi" - -#: system/ui/widgets/network.py:144 -#, python-format -msgid "Wi-Fi Network Metered" -msgstr "شبكة Wi‑Fi محدودة التعرفة" - -#: system/ui/widgets/network.py:314 -#, python-format -msgid "Wrong password" -msgstr "كلمة مرور خاطئة" - -#: selfdrive/ui/layouts/onboarding.py:145 -#, python-format -msgid "You must accept the Terms and Conditions in order to use openpilot." -msgstr "يجب عليك قبول الشروط والأحكام لاستخدام openpilot." - -#: selfdrive/ui/layouts/onboarding.py:112 -#, python-format -msgid "" -"You must accept the Terms and Conditions to use openpilot. Read the latest " -"terms at https://comma.ai/terms before continuing." -msgstr "" -"يجب عليك قبول الشروط والأحكام لاستخدام openpilot. اقرأ أحدث الشروط على " -"https://comma.ai/terms قبل المتابعة." - -#: selfdrive/ui/onroad/driver_camera_dialog.py:34 -#, python-format -msgid "camera starting" -msgstr "بدء تشغيل الكاميرا" - -#: selfdrive/ui/widgets/prime.py:63 -#, python-format -msgid "comma prime" -msgstr "comma prime" - -#: system/ui/widgets/network.py:142 -#, python-format -msgid "default" -msgstr "افتراضي" - -#: selfdrive/ui/layouts/settings/device.py:133 -#, python-format -msgid "down" -msgstr "أسفل" - -#: selfdrive/ui/layouts/settings/software.py:106 -#, python-format -msgid "failed to check for update" -msgstr "فشل التحقق من وجود تحديث" - -#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 -#, python-format -msgid "for \"{}\"" -msgstr "لـ \"{}\"" - -#: selfdrive/ui/onroad/hud_renderer.py:177 -#, python-format -msgid "km/h" -msgstr "كم/س" - -#: system/ui/widgets/network.py:204 -#, python-format -msgid "leave blank for automatic configuration" -msgstr "اتركه فارغاً للإعداد التلقائي" - -#: selfdrive/ui/layouts/settings/device.py:134 -#, python-format -msgid "left" -msgstr "يسار" - -#: system/ui/widgets/network.py:142 -#, python-format -msgid "metered" -msgstr "محدود التعرفة" - -#: selfdrive/ui/onroad/hud_renderer.py:177 -#, python-format -msgid "mph" -msgstr "ميل/س" - -#: selfdrive/ui/layouts/settings/software.py:20 -#, python-format -msgid "never" -msgstr "أبداً" - -#: selfdrive/ui/layouts/settings/software.py:31 -#, python-format -msgid "now" -msgstr "الآن" - -#: selfdrive/ui/layouts/settings/developer.py:71 -#, python-format -msgid "openpilot Longitudinal Control (Alpha)" -msgstr "التحكم الطولي لـ openpilot (ألفا)" - -#: selfdrive/ui/onroad/alert_renderer.py:51 -#, python-format -msgid "openpilot Unavailable" -msgstr "openpilot غير متاح" - -#: selfdrive/ui/layouts/settings/toggles.py:158 -#, python-format -msgid "" -"openpilot defaults to driving in chill mode. Experimental mode enables alpha-" -"level features that aren't ready for chill mode. Experimental features are " -"listed below:

End-to-End Longitudinal Control


Let the driving " -"model control the gas and brakes. 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


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." -msgstr "" -"يعمل openpilot افتراضياً في وضع الهدوء. يفعّل وضع التجربة ميزات بمستوى ألفا " -"غير الجاهزة لوضع الهدوء. الميزات التجريبية مدرجة أدناه:

التحكم الطولي " -"من طرف لطرف


دع نموذج القيادة يتحكم في الوقود والفرامل. سيقود " -"openpilot كما يظن أن الإنسان سيقود، بما في ذلك التوقف عند الإشارات الحمراء " -"وعلامات التوقف. بما أن نموذج القيادة يقرر السرعة، فإن السرعة المضبوطة تعمل " -"كحد أعلى فقط. هذه ميزة بجودة ألفا؛ يُتوقع حدوث أخطاء.

تصوير قيادة " -"جديد


سينتقل عرض القيادة إلى الكاميرا الواسعة المواجهة للطريق عند " -"السرعات المنخفضة لإظهار بعض المنعطفات بشكل أفضل. كما سيظهر شعار وضع التجربة " -"في الزاوية العلوية اليمنى." - -#: selfdrive/ui/layouts/settings/device.py:165 -#, python-format -msgid "" -"openpilot is continuously calibrating, resetting is rarely required. " -"Resetting calibration will restart openpilot if the car is powered on." -msgstr "" -"يقوم openpilot بالمعايرة بشكل مستمر، ونادراً ما تتطلب إعادة الضبط. ستؤدي " -"إعادة ضبط المعايرة إلى إعادة تشغيل openpilot إذا كانت السيارة قيد التشغيل." - -#: selfdrive/ui/layouts/settings/firehose.py:20 -msgid "" -"openpilot learns to drive by watching humans, like you, drive.\n" -"\n" -"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." -msgstr "" -"يتعلم openpilot القيادة بمشاهدة البشر، مثلك، يقودون.\n" -"\n" -"يتيح وضع Firehose زيادة تحميل بيانات التدريب لتحسين نماذج قيادة openpilot. " -"المزيد من البيانات يعني نماذج أكبر، مما يعني وضع تجربة أفضل." - -#: selfdrive/ui/layouts/settings/toggles.py:183 -#, python-format -msgid "openpilot longitudinal control may come in a future update." -msgstr "قد يأتي التحكم الطولي لـ openpilot في تحديث مستقبلي." - -#: selfdrive/ui/layouts/settings/device.py:26 -msgid "" -"openpilot requires the device to be mounted within 4° left or right and " -"within 5° up or 9° down." -msgstr "" -"يتطلب openpilot تركيب الجهاز ضمن 4° يساراً أو يميناً وضمن 5° للأعلى أو 9° " -"للأسفل." - -#: selfdrive/ui/layouts/settings/device.py:134 -#, python-format -msgid "right" -msgstr "يمين" - -#: system/ui/widgets/network.py:142 -#, python-format -msgid "unmetered" -msgstr "غير محدود" - -#: selfdrive/ui/layouts/settings/device.py:133 -#, python-format -msgid "up" -msgstr "أعلى" - -#: selfdrive/ui/layouts/settings/software.py:117 -#, python-format -msgid "up to date, last checked never" -msgstr "محدّث، آخر تحقق: أبداً" - -#: selfdrive/ui/layouts/settings/software.py:115 -#, python-format -msgid "up to date, last checked {}" -msgstr "محدّث، آخر تحقق {}" - -#: selfdrive/ui/layouts/settings/software.py:109 -#, python-format -msgid "update available" -msgstr "تحديث متاح" - -#: selfdrive/ui/layouts/home.py:169 -#, python-format -msgid "{} ALERT" -msgid_plural "{} ALERTS" -msgstr[0] "{} تنبيه" -msgstr[1] "{} تنبيه" -msgstr[2] "{} تنبيهان" -msgstr[3] "{} تنبيهات" -msgstr[4] "{} تنبيهات" -msgstr[5] "{} تنبيه" - -#: selfdrive/ui/layouts/settings/software.py:40 -#, python-format -msgid "{} day ago" -msgid_plural "{} days ago" -msgstr[0] "قبل {} يوم" -msgstr[1] "قبل {} يوم" -msgstr[2] "قبل {} يومين" -msgstr[3] "قبل {} أيام" -msgstr[4] "قبل {} أيام" -msgstr[5] "قبل {} يوم" - -#: selfdrive/ui/layouts/settings/software.py:37 -#, python-format -msgid "{} hour ago" -msgid_plural "{} hours ago" -msgstr[0] "قبل {} ساعة" -msgstr[1] "قبل {} ساعة" -msgstr[2] "قبل {} ساعتين" -msgstr[3] "قبل {} ساعات" -msgstr[4] "قبل {} ساعات" -msgstr[5] "قبل {} ساعة" - -#: selfdrive/ui/layouts/settings/software.py:34 -#, python-format -msgid "{} minute ago" -msgid_plural "{} minutes ago" -msgstr[0] "قبل {} دقيقة" -msgstr[1] "قبل {} دقيقة" -msgstr[2] "قبل {} دقيقتين" -msgstr[3] "قبل {} دقائق" -msgstr[4] "قبل {} دقائق" -msgstr[5] "قبل {} دقيقة" - -#: selfdrive/ui/layouts/settings/firehose.py:111 -#, python-format -msgid "{} segment of your driving is in the training dataset so far." -msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "{} مقطع من قيادتك ضمن مجموعة بيانات التدريب حتى الآن." -msgstr[1] "{} مقطع من قيادتك ضمن مجموعة بيانات التدريب حتى الآن." -msgstr[2] "{} مقطعان من قيادتك ضمن مجموعة بيانات التدريب حتى الآن." -msgstr[3] "{} مقاطع من قيادتك ضمن مجموعة بيانات التدريب حتى الآن." -msgstr[4] "{} مقاطع من قيادتك ضمن مجموعة بيانات التدريب حتى الآن." -msgstr[5] "{} مقطع من قيادتك ضمن مجموعة بيانات التدريب حتى الآن." - -#: selfdrive/ui/widgets/prime.py:62 -#, python-format -msgid "✓ SUBSCRIBED" -msgstr "✓ مشترك" - -#: selfdrive/ui/widgets/setup.py:22 -#, python-format -msgid "🔥 Firehose Mode 🔥" -msgstr "🔥 وضع Firehose 🔥" diff --git a/selfdrive/ui/translations/app_fr.po b/selfdrive/ui/translations/app_fr.po index f883d4d485..409761588e 100644 --- a/selfdrive/ui/translations/app_fr.po +++ b/selfdrive/ui/translations/app_fr.po @@ -5,10 +5,10 @@ # msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" +"Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-10-23 00:50-0700\n" -"PO-Revision-Date: 2025-10-20 18:19-0700\n" +"PO-Revision-Date: 2026-01-24 12:37+0100\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: fr\n" @@ -16,21 +16,22 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 3.8\n" #: selfdrive/ui/layouts/settings/device.py:160 #, python-format msgid " Steering torque response calibration is complete." -msgstr "" +msgstr " L'étalonnage de la réponse du couple de direction est terminé." #: selfdrive/ui/layouts/settings/device.py:158 #, python-format msgid " Steering torque response calibration is {}% complete." -msgstr "" +msgstr " L'étalonnage de la réponse du couple de direction est terminé à {}%." #: selfdrive/ui/layouts/settings/device.py:133 #, python-format msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." -msgstr "" +msgstr " Votre appareil est orienté {:.1f}° {} et {:.1f}° {}." #: selfdrive/ui/layouts/sidebar.py:43 msgid "--" @@ -79,12 +80,13 @@ msgstr "" #: selfdrive/ui/layouts/settings/device.py:148 #, python-format msgid "

Steering lag calibration is complete." -msgstr "" +msgstr "

L'étalonnage du délai de réponse de la direction est terminé." #: selfdrive/ui/layouts/settings/device.py:146 #, python-format msgid "

Steering lag calibration is {}% complete." msgstr "" +"

L'étalonnage du délai de réponse de la direction est terminé à {}%." #: selfdrive/ui/layouts/settings/firehose.py:138 #, python-format @@ -107,7 +109,7 @@ msgstr "AJOUTER" #: system/ui/widgets/network.py:139 #, python-format msgid "APN Setting" -msgstr "" +msgstr "Paramètres APN" #: selfdrive/ui/widgets/offroad_alerts.py:109 #, python-format @@ -117,7 +119,7 @@ msgstr "Accuser réception d'actionnement excessif" #: system/ui/widgets/network.py:74 system/ui/widgets/network.py:95 #, python-format msgid "Advanced" -msgstr "" +msgstr "Avancé" #: selfdrive/ui/layouts/settings/toggles.py:98 #, python-format @@ -208,18 +210,18 @@ msgstr "CONNECTER" #: system/ui/widgets/network.py:369 #, python-format msgid "CONNECTING..." -msgstr "CONNECTER" +msgstr "CONNECTER..." #: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 #: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 #, python-format msgid "Cancel" -msgstr "" +msgstr "Annuler" #: system/ui/widgets/network.py:134 #, python-format msgid "Cellular Metered" -msgstr "" +msgstr "Données cellulaire limitées" #: selfdrive/ui/layouts/settings/device.py:68 #, python-format @@ -230,7 +232,7 @@ msgstr "Changer la langue" #, python-format msgid "Changing this setting will restart openpilot if the car is powered on." msgstr "" -" La modification de ce réglage redémarrera openpilot si la voiture est sous " +"La modification de ce réglage redémarrera openpilot si la voiture est sous " "tension." #: selfdrive/ui/widgets/pairing_dialog.py:129 @@ -318,7 +320,7 @@ msgstr "Personnalité de conduite" #: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 #, python-format msgid "EDIT" -msgstr "" +msgstr "EDITER" #: selfdrive/ui/layouts/sidebar.py:138 msgid "ERROR" @@ -387,22 +389,22 @@ msgstr "" #: system/ui/widgets/network.py:204 #, python-format msgid "Enter APN" -msgstr "" +msgstr "Saisir l'APN" #: system/ui/widgets/network.py:241 #, python-format msgid "Enter SSID" -msgstr "" +msgstr "Entrer le SSID" #: system/ui/widgets/network.py:254 #, python-format msgid "Enter new tethering password" -msgstr "" +msgstr "Saisir le mot de passe du partage de connexion" #: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 #, python-format msgid "Enter password" -msgstr "" +msgstr "Saisir le mot de passe" #: selfdrive/ui/widgets/ssh_key.py:89 #, python-format @@ -412,7 +414,7 @@ msgstr "Entrez votre nom d'utilisateur GitHub" #: system/ui/widgets/list_view.py:123 system/ui/widgets/list_view.py:160 #, python-format msgid "Error" -msgstr "" +msgstr "Erreur" #: selfdrive/ui/layouts/settings/toggles.py:52 #, python-format @@ -431,7 +433,7 @@ msgstr "" #: system/ui/widgets/network.py:373 #, python-format msgid "FORGETTING..." -msgstr "" +msgstr "OUBLIER..." #: selfdrive/ui/widgets/setup.py:44 #, python-format @@ -493,12 +495,12 @@ msgstr "" #: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 #, python-format msgid "Forget" -msgstr "" +msgstr "Oublier" #: system/ui/widgets/network.py:319 #, python-format msgid "Forget Wi-Fi Network \"{}\"?" -msgstr "" +msgstr "Oublier le réseau Wi-Fi \"{}\" ?" #: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 msgid "GOOD" @@ -532,7 +534,7 @@ msgstr "INSTALLER" #: system/ui/widgets/network.py:150 #, python-format msgid "IP Address" -msgstr "" +msgstr "Adresse IP" #: selfdrive/ui/layouts/settings/software.py:53 #, python-format @@ -574,7 +576,7 @@ msgstr "" #: selfdrive/ui/layouts/settings/device.py:60 #, python-format msgid "N/A" -msgstr "" +msgstr "NC" #: selfdrive/ui/layouts/sidebar.py:142 msgid "NO" @@ -592,7 +594,7 @@ msgstr "Aucune clé SSH trouvée" #: selfdrive/ui/widgets/ssh_key.py:126 #, python-format msgid "No SSH keys found for user '{}'" -msgstr "Aucune clé SSH trouvée pour l'utilisateur '{username}'" +msgstr "Aucune clé SSH trouvée pour l'utilisateur '{}'" #: selfdrive/ui/widgets/offroad_alerts.py:320 #, python-format @@ -677,11 +679,15 @@ msgstr "Éteindre" #, python-format msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "" +"Eviter les transferts de données volumineux lorsque vous êtes connecté à un " +"réseau Wi-Fi limité" #: system/ui/widgets/network.py:135 #, python-format msgid "Prevent large data uploads when on a metered cellular connection" msgstr "" +"Eviter les transferts de données volumineux lors d'une connexion à un réseau " +"cellulaire limité" #: selfdrive/ui/layouts/settings/device.py:25 msgid "" @@ -802,32 +808,32 @@ msgstr "Consultez les règles, fonctionnalités et limitations d'openpilot" #: selfdrive/ui/layouts/settings/software.py:61 #, python-format msgid "SELECT" -msgstr "" +msgstr "SELECTIONNER" #: selfdrive/ui/layouts/settings/developer.py:53 #, python-format msgid "SSH Keys" -msgstr "" +msgstr "Clefs SSH" #: system/ui/widgets/network.py:310 #, python-format msgid "Scanning Wi-Fi networks..." -msgstr "" +msgstr "Analyse des réseaux Wi-Fi..." #: system/ui/widgets/option_dialog.py:36 #, python-format msgid "Select" -msgstr "" +msgstr "Sélectionner" #: selfdrive/ui/layouts/settings/software.py:183 #, python-format msgid "Select a branch" -msgstr "" +msgstr "Sélectionner une branche" #: selfdrive/ui/layouts/settings/device.py:91 #, python-format msgid "Select a language" -msgstr "" +msgstr "Sélectionner un langage" #: selfdrive/ui/layouts/settings/device.py:60 #, python-format @@ -880,12 +886,12 @@ msgstr "TEMPÉRATURE" #: selfdrive/ui/layouts/settings/software.py:61 #, python-format msgid "Target Branch" -msgstr "" +msgstr "Branche cible" #: system/ui/widgets/network.py:124 #, python-format msgid "Tethering Password" -msgstr "" +msgstr "Mot de passe du partage de connexion" #: selfdrive/ui/layouts/settings/settings.py:64 msgid "Toggles" @@ -986,12 +992,12 @@ msgstr "Wi‑Fi" #: system/ui/widgets/network.py:144 #, python-format msgid "Wi-Fi Network Metered" -msgstr "" +msgstr "Réseau Wi-Fi limité" #: system/ui/widgets/network.py:314 #, python-format msgid "Wrong password" -msgstr "" +msgstr "Mauvais mot de passe" #: selfdrive/ui/layouts/onboarding.py:145 #, python-format @@ -1020,12 +1026,12 @@ msgstr "comma prime" #: system/ui/widgets/network.py:142 #, python-format msgid "default" -msgstr "" +msgstr "défaut" #: selfdrive/ui/layouts/settings/device.py:133 #, python-format msgid "down" -msgstr "" +msgstr "bas" #: selfdrive/ui/layouts/settings/software.py:106 #, python-format @@ -1035,7 +1041,7 @@ msgstr "échec de la vérification de mise à jour" #: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 #, python-format msgid "for \"{}\"" -msgstr "" +msgstr "pour \"{}\"" #: selfdrive/ui/onroad/hud_renderer.py:177 #, python-format @@ -1045,17 +1051,17 @@ msgstr "km/h" #: system/ui/widgets/network.py:204 #, python-format msgid "leave blank for automatic configuration" -msgstr "" +msgstr "ne pas remplir pour une configuration automatique" #: selfdrive/ui/layouts/settings/device.py:134 #, python-format msgid "left" -msgstr "" +msgstr "gauche" #: system/ui/widgets/network.py:142 #, python-format msgid "metered" -msgstr "" +msgstr "limité" #: selfdrive/ui/onroad/hud_renderer.py:177 #, python-format @@ -1116,7 +1122,7 @@ msgid "" "openpilot is continuously calibrating, resetting is rarely required. " "Resetting calibration will restart openpilot if the car is powered on." msgstr "" -" La modification de ce réglage redémarrera openpilot si la voiture est sous " +"La modification de ce réglage redémarrera openpilot si la voiture est sous " "tension." #: selfdrive/ui/layouts/settings/firehose.py:20 @@ -1153,17 +1159,17 @@ msgstr "" #: selfdrive/ui/layouts/settings/device.py:134 #, python-format msgid "right" -msgstr "" +msgstr "droite" #: system/ui/widgets/network.py:142 #, python-format msgid "unmetered" -msgstr "" +msgstr "non limité" #: selfdrive/ui/layouts/settings/device.py:133 #, python-format msgid "up" -msgstr "" +msgstr "haut" #: selfdrive/ui/layouts/settings/software.py:117 #, python-format diff --git a/selfdrive/ui/translations/languages.json b/selfdrive/ui/translations/languages.json index 47e673ce89..99d3aafe3a 100644 --- a/selfdrive/ui/translations/languages.json +++ b/selfdrive/ui/translations/languages.json @@ -6,7 +6,6 @@ "Español": "es", "Türkçe": "tr", "Українська": "uk", - "العربية": "ar", "ไทย": "th", "中文(繁體)": "zh-CHT", "中文(简体)": "zh-CHS", diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index a9127ac3c5..f7a5d44d9a 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -299,7 +299,7 @@ class Device(DeviceSP): def _set_awake(self, on: bool): if on != self._awake: - DeviceSP._set_awake(self, on) + DeviceSP._set_awake(on, ui_state) self._awake = on cloudlog.debug(f"setting display power {int(on)}") HARDWARE.set_display_power(on) diff --git a/sunnypilot/modeld_v2/SConscript b/sunnypilot/modeld_v2/SConscript index 48b9c75ef5..28d39a75f1 100644 --- a/sunnypilot/modeld_v2/SConscript +++ b/sunnypilot/modeld_v2/SConscript @@ -39,7 +39,7 @@ if PC: model_dir = Dir("models").abspath cmd = f'python3 {Dir("#sunnypilot/modeld_v2").abspath}/install_models_pc.py {model_dir}' - for model_name in ['supercombo', 'driving_vision', 'driving_policy']: + for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']: if File(f"models/{model_name}.onnx").exists(): inputs.append(File(f"models/{model_name}.onnx")) inputs.append(File(f"models/{model_name}_tinygrad.pkl")) @@ -57,10 +57,10 @@ def tg_compile(flags, model_name): ) # Compile small models -for model_name in ['supercombo', 'driving_vision', 'driving_policy']: +for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']: if File(f"models/{model_name}.onnx").exists(): flags = { - 'larch64': 'DEV=QCOM', + 'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 IMAGE=2 JIT_BATCH_SIZE=0', 'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env }.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0') tg_compile(flags, model_name) diff --git a/sunnypilot/modeld_v2/install_models_pc.py b/sunnypilot/modeld_v2/install_models_pc.py index 3f964dc285..a378d90b11 100755 --- a/sunnypilot/modeld_v2/install_models_pc.py +++ b/sunnypilot/modeld_v2/install_models_pc.py @@ -44,7 +44,7 @@ def generate_metadata_pkl(model_path, output_path): def install_models(model_dir): model_dir = Path(model_dir) - models = ["driving_policy", "driving_vision"] + models = ["driving_off_policy", "driving_policy", "driving_vision"] found_models = [] for model in models: diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index be0724db0d..e25078f52f 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -1,4 +1,11 @@ #!/usr/bin/env python3 +import os +from openpilot.system.hardware import TICI +os.environ['DEV'] = 'QCOM' if TICI else 'CPU' +USBGPU = "USBGPU" in os.environ +if USBGPU: + os.environ['DEV'] = 'AMD' + os.environ['AMD_IFACE'] = 'USB' import time import numpy as np import cereal.messaging as messaging diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index a917d6cbb8..0de7496578 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -116,7 +116,7 @@ class ModelCache: class ModelFetcher: """Handles fetching and caching of model data from remote source""" - MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v10.json" + MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v14.json" def __init__(self, params: Params): self.params = params diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index ce6625a1f0..7fcf7f85ef 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -19,8 +19,8 @@ from openpilot.system.hardware.hw import Paths from pathlib import Path # see the README.md for more details on the model selector versioning -CURRENT_SELECTOR_VERSION = 13 -REQUIRED_MIN_SELECTOR_VERSION = 12 +CURRENT_SELECTOR_VERSION = 14 +REQUIRED_MIN_SELECTOR_VERSION = 14 USE_ONNX = os.getenv('USE_ONNX', PC) diff --git a/sunnypilot/models/runners/model_runner.py b/sunnypilot/models/runners/model_runner.py index 6902ed09b8..a49ff4d206 100644 --- a/sunnypilot/models/runners/model_runner.py +++ b/sunnypilot/models/runners/model_runner.py @@ -1,9 +1,7 @@ -import os from abc import abstractmethod, ABC import numpy as np from openpilot.sunnypilot.models.helpers import get_active_bundle -from openpilot.system.hardware import TICI from openpilot.sunnypilot.models.runners.constants import NumpyDict, ShapeDict, CLMemDict, FrameDict, Model, SliceDict, SEND_RAW_PRED from openpilot.system.hardware.hw import Paths import pickle @@ -11,15 +9,6 @@ import pickle CUSTOM_MODEL_PATH = Paths.model_root() -# Set QCOM environment variable for TICI devices, potentially enabling hardware acceleration -USBGPU = "USBGPU" in os.environ -if USBGPU: - os.environ['DEV'] = 'AMD' - os.environ['AMD_IFACE'] = 'USB' -else: - os.environ['DEV'] = 'QCOM' if TICI else 'CPU' - - class ModelData: """ Stores metadata and configuration for a specific machine learning model. diff --git a/sunnypilot/models/runners/tinygrad/model_types.py b/sunnypilot/models/runners/tinygrad/model_types.py index ba388aed93..11f0965828 100644 --- a/sunnypilot/models/runners/tinygrad/model_types.py +++ b/sunnypilot/models/runners/tinygrad/model_types.py @@ -13,6 +13,22 @@ SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') CUSTOM_MODEL_PATH = Paths.model_root() +class OffPolicyTinygrad(ModularRunner, ABC): + """ + A TinygradRunner specialized for off-policy models. + + Uses a SplitParser to handle outputs specific to the off-policy part of a split model setup. + """ + def __init__(self): + self._off_policy_parser = SplitParser() + self.parser_method_dict[ModelType.offPolicy] = self._parse_off_policy_outputs + + def _parse_off_policy_outputs(self, model_outputs: np.ndarray) -> NumpyDict: + """Parses off-policy model outputs using SplitParser.""" + result: NumpyDict = self._off_policy_parser.parse_policy_outputs(self._slice_outputs(model_outputs)) + return result + + class PolicyTinygrad(ModularRunner, ABC): """ A TinygradRunner specialized for policy-only models. diff --git a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py index 2800179fb2..2df1c65e08 100644 --- a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py +++ b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py @@ -4,7 +4,7 @@ import numpy as np from openpilot.sunnypilot.modeld_v2.runners.tinygrad_helpers import qcom_tensor_from_opencl_address from openpilot.sunnypilot.models.runners.constants import CLMemDict, FrameDict, NumpyDict, ModelType, ShapeDict, CUSTOM_MODEL_PATH, SliceDict from openpilot.sunnypilot.models.runners.model_runner import ModelRunner -from openpilot.sunnypilot.models.runners.tinygrad.model_types import PolicyTinygrad, VisionTinygrad, SupercomboTinygrad +from openpilot.sunnypilot.models.runners.tinygrad.model_types import PolicyTinygrad, VisionTinygrad, SupercomboTinygrad, OffPolicyTinygrad from openpilot.system.hardware import TICI from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants from openpilot.sunnypilot.modeld_v2.constants import ModelConstants @@ -12,7 +12,7 @@ from openpilot.sunnypilot.modeld_v2.constants import ModelConstants from tinygrad.tensor import Tensor -class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTinygrad): +class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTinygrad, OffPolicyTinygrad): """ A ModelRunner implementation for executing Tinygrad models. @@ -27,6 +27,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny SupercomboTinygrad.__init__(self) PolicyTinygrad.__init__(self) VisionTinygrad.__init__(self) + OffPolicyTinygrad.__init__(self) self._constants = ModelConstants self._model_data = self.models.get(model_type) if not self._model_data or not self._model_data.model: @@ -50,7 +51,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny self.input_to_dtype = {} self.input_to_device = {} for idx, name in enumerate(self.model_run.captured.expected_names): - info = self.model_run.captured.expected_st_vars_dtype_device[idx] + info = self.model_run.captured.expected_input_info[idx] self.input_to_dtype[name] = info[2] # dtype self.input_to_device[name] = info[3] # device @@ -83,7 +84,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny def _run_model(self) -> NumpyDict: """Runs the Tinygrad model inference and parses the outputs.""" - outputs = self.model_run(**self.inputs).contiguous().realize().uop.base.buffer.numpy() + outputs = self.model_run(**self.inputs).numpy().flatten() return self._parse_outputs(outputs) def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict: @@ -106,13 +107,20 @@ class TinygradSplitRunner(ModelRunner): self.is_20hz_3d = True self.vision_runner = TinygradRunner(ModelType.vision) self.policy_runner = TinygradRunner(ModelType.policy) + self.off_policy_runner = TinygradRunner(ModelType.offPolicy) if self.models.get(ModelType.offPolicy) else None self._constants = SplitModelConstants def _run_model(self) -> NumpyDict: """Runs both vision and policy models and merges their parsed outputs.""" policy_output = self.policy_runner.run_model() vision_output = self.vision_runner.run_model() - return {**policy_output, **vision_output} # Combine results + outputs = {**policy_output, **vision_output} + + if self.off_policy_runner: + off_policy_output = self.off_policy_runner.run_model() + outputs.update(off_policy_output) + + return outputs @property def vision_input_names(self) -> list[str]: @@ -122,12 +130,18 @@ class TinygradSplitRunner(ModelRunner): @property def input_shapes(self) -> ShapeDict: """Returns the combined input shapes from both vision and policy models.""" - return {**self.policy_runner.input_shapes, **self.vision_runner.input_shapes} + shapes = {**self.policy_runner.input_shapes, **self.vision_runner.input_shapes} + if self.off_policy_runner: + shapes.update(self.off_policy_runner.input_shapes) + return shapes @property def output_slices(self) -> SliceDict: """Returns the combined output slices from both vision and policy models.""" - return {**self.policy_runner.output_slices, **self.vision_runner.output_slices} + slices = {**self.policy_runner.output_slices, **self.vision_runner.output_slices} + if self.off_policy_runner: + slices.update(self.off_policy_runner.output_slices) + return slices def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict: """Prepares inputs for both vision and policy models.""" @@ -135,5 +149,11 @@ class TinygradSplitRunner(ModelRunner): self.policy_runner.prepare_policy_inputs(numpy_inputs) # Vision inputs depend on imgs_cl and frames self.vision_runner.prepare_vision_inputs(imgs_cl, frames) + inputs = {**self.policy_runner.inputs, **self.vision_runner.inputs} + + if self.off_policy_runner: + self.off_policy_runner.prepare_policy_inputs(numpy_inputs) + inputs.update(self.off_policy_runner.inputs) + # Return combined inputs (though they are stored within respective runners) - return {**self.policy_runner.inputs, **self.vision_runner.inputs} + return inputs diff --git a/sunnypilot/models/tests/model_hash b/sunnypilot/models/tests/model_hash index ab5ff86cef..f363f8309a 100644 --- a/sunnypilot/models/tests/model_hash +++ b/sunnypilot/models/tests/model_hash @@ -1 +1 @@ -0cde93a9d20f35619182e704261ef8f6a849149fe5849ba2d355b0b2c89af53c \ No newline at end of file +32f57bdc91f910df1f48ddae7c59aaf6e751f9df6756da481a210577dbce8bcf \ No newline at end of file diff --git a/sunnypilot/selfdrive/assets/icons/clock.png b/sunnypilot/selfdrive/assets/icons/clock.png new file mode 100644 index 0000000000..e04d1db949 --- /dev/null +++ b/sunnypilot/selfdrive/assets/icons/clock.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e095cfc4de71788bd4a99699a2e7ab4098cd426277d672b9e43981c5fab8b40f +size 16407 diff --git a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py index 2f27e7e42f..98757cd532 100644 --- a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py +++ b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py @@ -17,13 +17,16 @@ class BlinkerPauseLateral: self.enabled = self.params.get_bool("BlinkerPauseLateralControl") self.is_metric = self.params.get_bool("IsMetric") self.min_speed = 0 + self.reengage_delay = 0 + self.blinker_off_timer = 0.0 def get_params(self) -> None: self.enabled = self.params.get_bool("BlinkerPauseLateralControl") self.is_metric = self.params.get_bool("IsMetric") - self.min_speed = self.params.get("BlinkerMinLateralControlSpeed") + self.min_speed = self.params.get("BlinkerMinLateralControlSpeed", return_default=True) + self.reengage_delay = self.params.get("BlinkerLateralReengageDelay", return_default=True) - def update(self, CS: car.CarState) -> bool: + def update(self, CS: car.CarState, DT_CTRL: float = 0.01) -> bool: if not self.enabled: return False @@ -31,4 +34,11 @@ class BlinkerPauseLateral: speed_factor = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS min_speed_ms = self.min_speed * speed_factor - return bool(one_blinker and CS.vEgo < min_speed_ms) + below_speed = CS.vEgo < min_speed_ms + + if one_blinker and below_speed: + self.blinker_off_timer = self.reengage_delay + elif self.blinker_off_timer > 0: + self.blinker_off_timer -= DT_CTRL + + return bool((one_blinker and below_speed) or self.blinker_off_timer > 0) diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 68b50c3e19..6efda4585f 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -36,16 +36,12 @@ class LongitudinalPlannerSP: self.output_v_target = 0. self.output_a_target = 0. - @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: + def is_e2e(self, sm: messaging.SubMaster) -> bool: + experimental_mode = sm['selfdriveState'].experimentalMode if not self.dec.active(): - return None + return experimental_mode - return self.dec.mode() + return experimental_mode and self.dec.mode() == "blended" def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]: CS = sm['carState'] diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py index 9f6efffb55..d35b79a73b 100644 --- a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py +++ b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py @@ -5,6 +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 numpy as np +import pytest import cereal.messaging as messaging from cereal import custom, log @@ -12,11 +13,52 @@ from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.selfdrive.modeld.constants import ModelConstants -from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision +from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control import MIN_V +from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision, _ENTERING_PRED_LAT_ACC_TH VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.VisionState +def _th_above_f32(th: float) -> float: + """ + Return the next representable float32 *above* `th`. + This avoids flaky comparisons around thresholds due to float32 rounding. + """ + th32 = np.float32(th) + above32 = np.nextafter(th32, np.float32(np.inf), dtype=np.float32) + return float(above32) + + +def _build_single_spike_filtered(n: int, base: float = 1.0) -> np.ndarray: + """ + Create an array where max() is >= threshold but p97 is < threshold. + This demonstrates the behavior difference vs np.amax(). + + Note: We intentionally construct using float32-representable values to match + the data path through cereal/capnp. + """ + th = float(_ENTERING_PRED_LAT_ACC_TH) + th32 = float(np.float32(th)) + + # numpy percentile default is linear interpolation: idx=(n-1)*p/100 + idx = (n - 1) * 0.97 + w = float(idx - np.floor(idx)) + + base32 = float(np.float32(base)) + + # Choose spike so that p97 = base + w*(spike-base) < th + # -> spike < base + (th-base)/w. Use a margin (0.9) and ensure spike >= th. + if w == 0.0: + spike = th32 + 1.0 + else: + spike = base32 + (th32 - base32) / w * 0.9 + spike = max(spike, th32 + 0.01) + + arr = np.full(n, base32, dtype=np.float32) + arr[-1] = np.float32(spike) + return arr + + def generate_modelV2(): model = messaging.new_message('modelV2') position = log.XYZTData.new_message() @@ -101,4 +143,72 @@ class TestSmartCruiseControlVision: self.scc_v.update(self.sm, True, False, 0., 0., 0.) assert self.scc_v.state == VisionState.enabled + @pytest.mark.parametrize( + "case, should_enter", + [ + ("p97_just_above_threshold", True), + ("single_spike_filtered", False), + ("persistent_high_values", True), + ], + ids=[ + "p97>threshold_enters", + "single_spike_max_large_but_p97_below_threshold", + "high_values_persist_trigger_entering", + ], + ) + def test_max_pred_lat_acc_uses_p97_and_threshold(self, case, should_enter): + n = len(ModelConstants.T_IDXS) + th = float(_ENTERING_PRED_LAT_ACC_TH) + + if case == "p97_just_above_threshold": + # Use the next representable float32 above threshold to avoid float32 rounding flakiness. + val = _th_above_f32(th) + pred_lat_accels = np.full(n, np.float32(val), dtype=np.float32) + + elif case == "single_spike_filtered": + pred_lat_accels = _build_single_spike_filtered(n, base=1.0) + + elif case == "persistent_high_values": + # Make enough "high" samples so p97 is driven by the persistent trend, not a single outlier. + high_count = max(2, int(np.ceil(n * 0.03)) + 1) + pred_lat_accels = np.full(n, np.float32(1.0), dtype=np.float32) + pred_lat_accels[-high_count:] = np.float32(2.0) + pred_lat_accels[-1] = np.float32(8.0) # keep one big outlier too + + else: + raise AssertionError(f"Unknown case: {case}") + + # Override model predictions so: + # predicted_lat_accels = abs(orientationRate.z) * velocity.x == pred_lat_accels + mdl = generate_modelV2() + mdl.modelV2.velocity.x = [1.0 for _ in range(n)] + mdl.modelV2.orientationRate.z = [float(x) for x in pred_lat_accels] + self.sm["modelV2"] = mdl.modelV2 + + v_ego = float(MIN_V + 5.0) + + # 1st update: disabled -> enabled + self.scc_v.update(self.sm, True, False, v_ego, 0.0, 0.0) + # 2nd update: evaluate entering condition from enabled state + self.scc_v.update(self.sm, True, False, v_ego, 0.0, 0.0) + + # Controller does percentile on numpy float64 arrays (values already quantized by capnp), + # so compute expected in float64 to match behavior and avoid interpolation/rounding deltas. + expected_p97 = float(np.percentile(pred_lat_accels.astype(np.float64), 97)) + + # allow tiny numeric differences due to float conversions/interpolation + assert np.isclose(self.scc_v.max_pred_lat_acc, expected_p97, rtol=1e-6, atol=1e-5) + + if should_enter: + # We assert entering primarily by state (this is the actual intended behavior). + assert self.scc_v.state == VisionState.entering + # Optional sanity: should be >= threshold with some margin (since we used nextafter above threshold). + assert self.scc_v.max_pred_lat_acc > th + + else: + # Difference vs np.amax(): max can be above threshold, but p97 stays below it. + assert float(np.max(pred_lat_accels)) >= th + assert self.scc_v.max_pred_lat_acc < th + assert self.scc_v.state == VisionState.enabled + # TODO-SP: mock modelV2 data to test other states diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py index d144178e93..a9d2a66227 100644 --- a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py +++ b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py @@ -90,7 +90,7 @@ class SmartCruiseControlVision: # get the maximum lat accel from the model predicted_lat_accels = rate_plan * vel_plan - self.max_pred_lat_acc = np.amax(predicted_lat_accels) + self.max_pred_lat_acc = np.percentile(predicted_lat_accels, 97) # get the maximum curve based on the current velocity v_ego = max(self.v_ego, 0.1) # ensure a value greater than 0 for calculations diff --git a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py index ca8f16fe74..b547ea96b6 100644 --- a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py +++ b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py @@ -20,6 +20,8 @@ class TestBlinkerPauseLateral: self.blinker_pause_lateral.enabled = True self.blinker_pause_lateral.is_metric = False self.blinker_pause_lateral.min_speed = 20 # MPH + self.blinker_pause_lateral.reengage_delay = 0 + self.blinker_pause_lateral.blinker_off_timer = 0.0 self.CS = car.CarState.new_message() self.CS.vEgo = 0 @@ -46,6 +48,18 @@ class TestBlinkerPauseLateral: } self._test_should_blinker_pause_lateral(expected_results) + def test_reengage_delay(self): + self.blinker_pause_lateral.reengage_delay = 2 # seconds + self.CS.vEgo = 4.5 # ~10 MPH + + expected_results = { + (False, False): True, + (True, False): True, + (False, True): True, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + def test_above_min_speed_blinker(self): self.CS.vEgo = 13.4 # ~30 MPH diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json index 7f597a9621..ad94ad35ff 100644 --- a/sunnypilot/sunnylink/params_metadata.json +++ b/sunnypilot/sunnylink/params_metadata.json @@ -90,8 +90,12 @@ "description": "" }, "BlindSpot": { - "title": "Blind Spot Detection", - "description": "" + "title": "[TIZI/TICI only] Blind Spot Detection", + "description": "Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported." + }, + "BlinkerLateralReengageDelay": { + "title": "Post-Blinker Delay", + "description": "Delay before lateral control resumes after the turn signal ends." }, "BlinkerMinLateralControlSpeed": { "title": "Blinker Min Lateral Control Speed", @@ -339,8 +343,8 @@ "description": "" }, "GreenLightAlert": { - "title": "Green Light Alert", - "description": "" + "title": "Green Traffic Light Alert (Beta)", + "description": "A chime and on-screen alert (TIZI/TICI only) will play when the traffic light you are waiting for turns green and you have no vehicle in front of you.
Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly." }, "GsmApn": { "title": "GSM APN", @@ -367,8 +371,8 @@ "description": "" }, "HideVEgoUI": { - "title": "Hide vEgo UI", - "description": "" + "title": "[TIZI/TICI only] Speedometer: Hide from Onroad Screen", + "description": "When enabled, the speedometer on the onroad screen is not displayed." }, "HyundaiLongitudinalTuning": { "title": "Hyundai Longitudinal Tuning", @@ -585,8 +589,8 @@ "description": "" }, "LeadDepartAlert": { - "title": "Lead Depart Alert", - "description": "" + "title": "Lead Departure Alert (Beta)", + "description": "A chime and on-screen alert (TIZI/TICI only) will play when you are stopped, and the vehicle in front of you start moving.
Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly." }, "LiveDelay": { "title": "Live Delay", @@ -1079,12 +1083,12 @@ "description": "" }, "RoadNameToggle": { - "title": "Display Road Name", - "description": "" + "title": "[TIZI/TICI only] Display Road Name", + "description": "Displays the name of the road the car is traveling on.
The OpenStreetMap database of the location must be downloaded to fetch the road name." }, "RocketFuel": { - "title": "Display Rocket Fuel Bar", - "description": "Show an indicator on the left side of the screen to display real-time vehicle acceleration and deceleration." + "title": "[TIZI/TICI only] Real-time Acceleration Bar", + "description": "Show an indicator on the left side of the screen to display real-time vehicle acceleration and deceleration. This displays what the car is currently doing, not what the planner is requesting." }, "RouteCount": { "title": "Route Count", @@ -1103,8 +1107,8 @@ "description": "" }, "ShowTurnSignals": { - "title": "Show Turn Signals", - "description": "" + "title": "[TIZI/TICI only] Display Turn Signals", + "description": "When enabled, visual turn indicators are drawn on the HUD." }, "SmartCruiseControlMap": { "title": "Smart Cruise Control - Map", @@ -1196,8 +1200,8 @@ "description": "" }, "StandstillTimer": { - "title": "Standstill Timer", - "description": "" + "title": "[TIZI/TICI only] Standstill Timer", + "description": "Show a timer on the HUD when the car is at a standstill." }, "SubaruStopAndGo": { "title": "Subaru Stop and Go", @@ -1239,6 +1243,10 @@ "title": "Tesla Coop Steering", "description": "" }, + "TorqueBar": { + "title": "[TIZI/TICI only] Steering Arc", + "description": "Display steering arc on the driving screen when lateral control is enabled." + }, "TorqueParamsOverrideEnabled": { "title": "Manual Real-Time Tuning", "description": "" @@ -1267,8 +1275,8 @@ "description": "" }, "TrueVEgoUI": { - "title": "True vEgo UI", - "description": "" + "title": "[TIZI/TICI only] Speedometer: Always Display True Speed", + "description": "For applicable vehicles, always display the true vehicle current speed from wheel speed sensors." }, "UbloxAvailable": { "title": "Ublox Available", diff --git a/sunnypilot/sunnylink/sunnylink_state.py b/sunnypilot/sunnylink/sunnylink_state.py index 02b91c3cbc..927d041991 100644 --- a/sunnypilot/sunnylink/sunnylink_state.py +++ b/sunnypilot/sunnylink/sunnylink_state.py @@ -109,6 +109,8 @@ class SunnylinkState: self.sunnylink_dongle_id = self._params.get("SunnylinkDongleId") self._api = SunnylinkApi(self.sunnylink_dongle_id) + self._panel_open = False + self._load_initial_state() def _load_initial_state(self) -> None: @@ -166,9 +168,14 @@ class SunnylinkState: def _worker_thread(self) -> None: while self._running: - if self.is_connected(): - self._fetch_roles() - self._fetch_users() + with self._lock: + panel_open = self._panel_open + + if panel_open: + self._sm.update() + if self.is_connected(): + self._fetch_roles() + self._fetch_users() for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)): if not self._running: @@ -220,5 +227,9 @@ class SunnylinkState: else: return style.ITEM_TEXT_VALUE_COLOR + def set_settings_open(self, _open: bool) -> None: + with self._lock: + self._panel_open = _open + def __del__(self): self.stop() diff --git a/system/hardware/tici/amplifier.py b/system/hardware/tici/amplifier.py index d714837bb3..09436e6ff4 100755 --- a/system/hardware/tici/amplifier.py +++ b/system/hardware/tici/amplifier.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 import time -from smbus2 import SMBus from collections import namedtuple +from openpilot.common.i2c import SMBus + # https://datasheets.maximintegrated.com/en/ds/MAX98089.pdf AmpConfig = namedtuple('AmpConfig', ['name', 'value', 'register', 'offset', 'mask']) diff --git a/system/hardware/tici/tests/test_power_draw.py b/system/hardware/tici/tests/test_power_draw.py index 4fbde81673..a92e4c8de8 100644 --- a/system/hardware/tici/tests/test_power_draw.py +++ b/system/hardware/tici/tests/test_power_draw.py @@ -3,7 +3,7 @@ import pytest import time import numpy as np from dataclasses import dataclass -from tabulate import tabulate +from openpilot.common.utils import tabulate import cereal.messaging as messaging from cereal.services import SERVICE_LIST diff --git a/system/manager/manager.py b/system/manager/manager.py index 36e45488f6..8c5d35d072 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -20,6 +20,7 @@ from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_I from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.system.version import get_build_metadata from openpilot.system.hardware.hw import Paths +from openpilot.system.hardware import PC def manager_init() -> None: @@ -39,6 +40,12 @@ def manager_init() -> None: if params.get("DeviceBootMode") == 1: # start in Always Offroad mode params.put_bool("OffroadMode", True) + # quick boot + if params.get_bool("QuickBootToggle") and not PC: + prebuilt_path = "/data/openpilot/prebuilt" + if not os.path.exists(prebuilt_path): + open(prebuilt_path, 'x').close() + if params.get_bool("RecordFrontLock"): params.put_bool("RecordFront", True) diff --git a/system/proclogd.py b/system/proclogd.py index 3279425b7b..b008f8ed9b 100755 --- a/system/proclogd.py +++ b/system/proclogd.py @@ -115,6 +115,55 @@ def _parse_proc_stat(stat: str) -> ProcStat | None: cloudlog.exception("failed to parse /proc//stat") return None +class SmapsData(TypedDict): + pss: int # bytes + pss_anon: int # bytes + pss_shmem: int # bytes + + +_SMAPS_KEYS = {b'Pss:', b'Pss_Anon:', b'Pss_Shmem:'} + +# smaps_rollup (kernel 4.14+) is ideal but missing on some BSP kernels; +# fall back to per-VMA smaps (any kernel). Pss_Anon/Pss_Shmem only in 5.x+. +_smaps_path: str | None = None # auto-detected on first call + +# per-VMA smaps is expensive (kernel walks page tables for every VMA). +# cache results and only refresh every N cycles to keep CPU low. +_smaps_cache: dict[int, SmapsData] = {} +_smaps_cycle = 0 +_SMAPS_EVERY = 20 # refresh every 20th cycle (40s at 0.5Hz) + + +def _read_smaps(pid: int) -> SmapsData: + global _smaps_path + try: + if _smaps_path is None: + _smaps_path = 'smaps_rollup' if os.path.exists(f'/proc/{pid}/smaps_rollup') else 'smaps' + + result: SmapsData = {'pss': 0, 'pss_anon': 0, 'pss_shmem': 0} + with open(f'/proc/{pid}/{_smaps_path}', 'rb') as f: + for line in f: + parts = line.split() + if len(parts) >= 2 and parts[0] in _SMAPS_KEYS: + val = int(parts[1]) * 1024 # kB -> bytes + if parts[0] == b'Pss:': + result['pss'] += val + elif parts[0] == b'Pss_Anon:': + result['pss_anon'] += val + elif parts[0] == b'Pss_Shmem:': + result['pss_shmem'] += val + return result + except (FileNotFoundError, PermissionError, ProcessLookupError, OSError): + return {'pss': 0, 'pss_anon': 0, 'pss_shmem': 0} + + +def _get_smaps_cached(pid: int) -> SmapsData: + """Return cached smaps data, refreshing every _SMAPS_EVERY cycles.""" + if _smaps_cycle == 0 or pid not in _smaps_cache: + _smaps_cache[pid] = _read_smaps(pid) + return _smaps_cache.get(pid, {'pss': 0, 'pss_anon': 0, 'pss_shmem': 0}) + + class ProcExtra(TypedDict): pid: int name: str @@ -189,6 +238,13 @@ def build_proc_log_message(msg) -> None: for j, arg in enumerate(extra['cmdline']): cmdline[j] = arg + # smaps is expensive (kernel walks page tables); skip small processes, use cache + if r['rss'] * PAGE_SIZE > 5 * 1024 * 1024: + smaps = _get_smaps_cached(r['pid']) + proc.memPss = smaps['pss'] + proc.memPssAnon = smaps['pss_anon'] + proc.memPssShmem = smaps['pss_shmem'] + cpu_times = _cpu_times() cpu_list = pl.init('cpuTimes', len(cpu_times)) for i, ct in enumerate(cpu_times): @@ -212,6 +268,9 @@ def build_proc_log_message(msg) -> None: pl.mem.inactive = mem_info["Inactive:"] pl.mem.shared = mem_info["Shmem:"] + global _smaps_cycle + _smaps_cycle = (_smaps_cycle + 1) % _SMAPS_EVERY + def main() -> NoReturn: pm = messaging.PubMaster(['procLog']) diff --git a/system/sensord/sensors/i2c_sensor.py b/system/sensord/sensors/i2c_sensor.py index 336ebb1fd3..57edcc52d9 100644 --- a/system/sensord/sensors/i2c_sensor.py +++ b/system/sensord/sensors/i2c_sensor.py @@ -1,9 +1,10 @@ import time -import smbus2 import ctypes from collections.abc import Iterable from cereal import log +from openpilot.common.i2c import SMBus + class Sensor: class SensorException(Exception): @@ -13,7 +14,7 @@ class Sensor: pass def __init__(self, bus: int) -> None: - self.bus = smbus2.SMBus(bus) + self.bus = SMBus(bus) self.source = log.SensorEventData.SensorSource.velodyne # unknown self.start_ts = 0. diff --git a/system/ubloxd/SConscript b/system/ubloxd/SConscript deleted file mode 100644 index 9eb50760ba..0000000000 --- a/system/ubloxd/SConscript +++ /dev/null @@ -1,11 +0,0 @@ -Import('env') - -if GetOption('kaitai'): - current_dir = Dir('./generated/').srcnode().abspath - python_cmd = f"kaitai-struct-compiler --target python --outdir {current_dir} $SOURCES" - env.Command(File('./generated/ubx.py'), 'ubx.ksy', python_cmd) - env.Command(File('./generated/gps.py'), 'gps.ksy', python_cmd) - env.Command(File('./generated/glonass.py'), 'glonass.ksy', python_cmd) - # kaitai issue: https://github.com/kaitai-io/kaitai_struct/issues/910 - py_glonass_fix = env.Command(None, File('./generated/glonass.py'), "sed -i 's/self._io.align_to_byte()/# self._io.align_to_byte()/' $SOURCES") - env.Depends(py_glonass_fix, File('./generated/glonass.py')) diff --git a/system/ubloxd/binary_struct.py b/system/ubloxd/binary_struct.py new file mode 100644 index 0000000000..7b229620a2 --- /dev/null +++ b/system/ubloxd/binary_struct.py @@ -0,0 +1,280 @@ +""" +Binary struct parsing DSL. + +Defines a declarative schema for binary messages using dataclasses +and type annotations. +""" + +import struct +from enum import Enum +from dataclasses import dataclass, is_dataclass +from typing import Annotated, Any, TypeVar, get_args, get_origin + + +class FieldType: + """Base class for field type descriptors.""" + + +@dataclass(frozen=True) +class IntType(FieldType): + bits: int + signed: bool + big_endian: bool = False + +@dataclass(frozen=True) +class FloatType(FieldType): + bits: int + +@dataclass(frozen=True) +class BitsType(FieldType): + bits: int + +@dataclass(frozen=True) +class BytesType(FieldType): + size: int + +@dataclass(frozen=True) +class ArrayType(FieldType): + element_type: Any + count_field: str + +@dataclass(frozen=True) +class SwitchType(FieldType): + selector: str + cases: dict[Any, Any] + default: Any = None + +@dataclass(frozen=True) +class EnumType(FieldType): + base_type: FieldType + enum_cls: type[Enum] + +@dataclass(frozen=True) +class ConstType(FieldType): + base_type: FieldType + expected: Any + +@dataclass(frozen=True) +class SubstreamType(FieldType): + length_field: str + element_type: Any + +# Common types - little endian +u8 = IntType(8, False) +u16 = IntType(16, False) +u32 = IntType(32, False) +s8 = IntType(8, True) +s16 = IntType(16, True) +s32 = IntType(32, True) +f32 = FloatType(32) +f64 = FloatType(64) +# Big endian variants +u16be = IntType(16, False, big_endian=True) +u32be = IntType(32, False, big_endian=True) +s16be = IntType(16, True, big_endian=True) +s32be = IntType(32, True, big_endian=True) + + +def bits(n: int) -> BitsType: + """Create a bit-level field type.""" + return BitsType(n) + +def bytes_field(size: int) -> BytesType: + """Create a fixed-size bytes field.""" + return BytesType(size) + +def array(element_type: Any, count_field: str) -> ArrayType: + """Create an array/repeated field.""" + return ArrayType(element_type, count_field) + +def switch(selector: str, cases: dict[Any, Any], default: Any = None) -> SwitchType: + """Create a switch-on field.""" + return SwitchType(selector, cases, default) + +def enum(base_type: Any, enum_cls: type[Enum]) -> EnumType: + """Create an enum-wrapped field.""" + field_type = _field_type_from_spec(base_type) + if field_type is None: + raise TypeError(f"Unsupported field type: {base_type!r}") + return EnumType(field_type, enum_cls) + +def const(base_type: Any, expected: Any) -> ConstType: + """Create a constant-value field.""" + field_type = _field_type_from_spec(base_type) + if field_type is None: + raise TypeError(f"Unsupported field type: {base_type!r}") + return ConstType(field_type, expected) + +def substream(length_field: str, element_type: Any) -> SubstreamType: + """Parse a fixed-length substream using an inner schema.""" + return SubstreamType(length_field, element_type) + + +class BinaryReader: + def __init__(self, data: bytes): + self.data = data + self.pos = 0 + self.bit_pos = 0 # 0-7, position within current byte + + def _require(self, n: int) -> None: + if self.pos + n > len(self.data): + raise EOFError("Unexpected end of data") + + def _read_struct(self, fmt: str): + self._align_to_byte() + size = struct.calcsize(fmt) + self._require(size) + value = struct.unpack_from(fmt, self.data, self.pos)[0] + self.pos += size + return value + + def read_bytes(self, n: int) -> bytes: + self._align_to_byte() + self._require(n) + result = self.data[self.pos : self.pos + n] + self.pos += n + return result + + def read_bits_int_be(self, n: int) -> int: + result = 0 + bits_remaining = n + while bits_remaining > 0: + if self.pos >= len(self.data): + raise EOFError("Unexpected end of data while reading bits") + bits_in_byte = 8 - self.bit_pos + bits_to_read = min(bits_remaining, bits_in_byte) + byte_val = self.data[self.pos] + shift = bits_in_byte - bits_to_read + mask = (1 << bits_to_read) - 1 + extracted = (byte_val >> shift) & mask + result = (result << bits_to_read) | extracted + self.bit_pos += bits_to_read + bits_remaining -= bits_to_read + if self.bit_pos >= 8: + self.bit_pos = 0 + self.pos += 1 + return result + + def _align_to_byte(self) -> None: + if self.bit_pos > 0: + self.bit_pos = 0 + self.pos += 1 + + +T = TypeVar('T', bound='BinaryStruct') + + +class BinaryStruct: + """Base class for binary struct definitions.""" + + def __init_subclass__(cls, **kwargs) -> None: + super().__init_subclass__(**kwargs) + if cls is BinaryStruct: + return + if not is_dataclass(cls): + dataclass(init=False)(cls) + fields = list(getattr(cls, '__annotations__', {}).items()) + cls.__binary_fields__ = fields # type: ignore[attr-defined] + + @classmethod + def _read(inner_cls, reader: BinaryReader): + obj = inner_cls.__new__(inner_cls) + for name, spec in inner_cls.__binary_fields__: + value = _parse_field(spec, reader, obj) + setattr(obj, name, value) + return obj + + cls._read = _read # type: ignore[attr-defined] + + @classmethod + def from_bytes(cls: type[T], data: bytes) -> T: + """Parse struct from bytes.""" + reader = BinaryReader(data) + return cls._read(reader) + + @classmethod + def _read(cls: type[T], reader: BinaryReader) -> T: + """Override in subclasses to implement parsing.""" + raise NotImplementedError + + +def _resolve_path(obj: Any, path: str) -> Any: + cur = obj + for part in path.split('.'): + cur = getattr(cur, part) + return cur + +def _unwrap_annotated(spec: Any) -> tuple[Any, ...]: + if get_origin(spec) is Annotated: + return get_args(spec)[1:] + return () + +def _field_type_from_spec(spec: Any) -> FieldType | None: + if isinstance(spec, FieldType): + return spec + for item in _unwrap_annotated(spec): + if isinstance(item, FieldType): + return item + return None + + +def _int_format(field_type: IntType) -> str: + if field_type.bits == 8: + return 'b' if field_type.signed else 'B' + endian = '>' if field_type.big_endian else '<' + if field_type.bits == 16: + code = 'h' if field_type.signed else 'H' + elif field_type.bits == 32: + code = 'i' if field_type.signed else 'I' + else: + raise ValueError(f"Unsupported integer size: {field_type.bits}") + return f"{endian}{code}" + +def _float_format(field_type: FloatType) -> str: + if field_type.bits == 32: + return ' Any: + field_type = _field_type_from_spec(spec) + if field_type is not None: + spec = field_type + if isinstance(spec, ConstType): + value = _parse_field(spec.base_type, reader, obj) + if value != spec.expected: + raise ValueError(f"Invalid constant: expected {spec.expected!r}, got {value!r}") + return value + if isinstance(spec, EnumType): + raw = _parse_field(spec.base_type, reader, obj) + try: + return spec.enum_cls(raw) + except ValueError: + return raw + if isinstance(spec, SwitchType): + key = _resolve_path(obj, spec.selector) + target = spec.cases.get(key, spec.default) + if target is None: + return None + return _parse_field(target, reader, obj) + if isinstance(spec, ArrayType): + count = _resolve_path(obj, spec.count_field) + return [_parse_field(spec.element_type, reader, obj) for _ in range(int(count))] + if isinstance(spec, SubstreamType): + length = _resolve_path(obj, spec.length_field) + data = reader.read_bytes(int(length)) + sub_reader = BinaryReader(data) + return _parse_field(spec.element_type, sub_reader, obj) + if isinstance(spec, IntType): + return reader._read_struct(_int_format(spec)) + if isinstance(spec, FloatType): + return reader._read_struct(_float_format(spec)) + if isinstance(spec, BitsType): + value = reader.read_bits_int_be(spec.bits) + return bool(value) if spec.bits == 1 else value + if isinstance(spec, BytesType): + return reader.read_bytes(spec.size) + if isinstance(spec, type) and issubclass(spec, BinaryStruct): + return spec._read(reader) + raise TypeError(f"Unsupported field spec: {spec!r}") diff --git a/system/ubloxd/generated/glonass.py b/system/ubloxd/generated/glonass.py deleted file mode 100644 index 40aa16bb6f..0000000000 --- a/system/ubloxd/generated/glonass.py +++ /dev/null @@ -1,247 +0,0 @@ -# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -import kaitaistruct -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO - - -if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): - raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) - -class Glonass(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.idle_chip = self._io.read_bits_int_be(1) != 0 - self.string_number = self._io.read_bits_int_be(4) - # workaround for kaitai bit alignment issue (see glonass_fix.patch for C++) - # self._io.align_to_byte() - _on = self.string_number - if _on == 4: - self.data = Glonass.String4(self._io, self, self._root) - elif _on == 1: - self.data = Glonass.String1(self._io, self, self._root) - elif _on == 3: - self.data = Glonass.String3(self._io, self, self._root) - elif _on == 5: - self.data = Glonass.String5(self._io, self, self._root) - elif _on == 2: - self.data = Glonass.String2(self._io, self, self._root) - else: - self.data = Glonass.StringNonImmediate(self._io, self, self._root) - self.hamming_code = self._io.read_bits_int_be(8) - self.pad_1 = self._io.read_bits_int_be(11) - self.superframe_number = self._io.read_bits_int_be(16) - self.pad_2 = self._io.read_bits_int_be(8) - self.frame_number = self._io.read_bits_int_be(8) - - class String4(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.tau_n_sign = self._io.read_bits_int_be(1) != 0 - self.tau_n_value = self._io.read_bits_int_be(21) - self.delta_tau_n_sign = self._io.read_bits_int_be(1) != 0 - self.delta_tau_n_value = self._io.read_bits_int_be(4) - self.e_n = self._io.read_bits_int_be(5) - self.not_used_1 = self._io.read_bits_int_be(14) - self.p4 = self._io.read_bits_int_be(1) != 0 - self.f_t = self._io.read_bits_int_be(4) - self.not_used_2 = self._io.read_bits_int_be(3) - self.n_t = self._io.read_bits_int_be(11) - self.n = self._io.read_bits_int_be(5) - self.m = self._io.read_bits_int_be(2) - - @property - def tau_n(self): - if hasattr(self, '_m_tau_n'): - return self._m_tau_n - - self._m_tau_n = ((self.tau_n_value * -1) if self.tau_n_sign else self.tau_n_value) - return getattr(self, '_m_tau_n', None) - - @property - def delta_tau_n(self): - if hasattr(self, '_m_delta_tau_n'): - return self._m_delta_tau_n - - self._m_delta_tau_n = ((self.delta_tau_n_value * -1) if self.delta_tau_n_sign else self.delta_tau_n_value) - return getattr(self, '_m_delta_tau_n', None) - - - class StringNonImmediate(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.data_1 = self._io.read_bits_int_be(64) - self.data_2 = self._io.read_bits_int_be(8) - - - class String5(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.n_a = self._io.read_bits_int_be(11) - self.tau_c = self._io.read_bits_int_be(32) - self.not_used = self._io.read_bits_int_be(1) != 0 - self.n_4 = self._io.read_bits_int_be(5) - self.tau_gps = self._io.read_bits_int_be(22) - self.l_n = self._io.read_bits_int_be(1) != 0 - - - class String1(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.not_used = self._io.read_bits_int_be(2) - self.p1 = self._io.read_bits_int_be(2) - self.t_k = self._io.read_bits_int_be(12) - self.x_vel_sign = self._io.read_bits_int_be(1) != 0 - self.x_vel_value = self._io.read_bits_int_be(23) - self.x_accel_sign = self._io.read_bits_int_be(1) != 0 - self.x_accel_value = self._io.read_bits_int_be(4) - self.x_sign = self._io.read_bits_int_be(1) != 0 - self.x_value = self._io.read_bits_int_be(26) - - @property - def x_vel(self): - if hasattr(self, '_m_x_vel'): - return self._m_x_vel - - self._m_x_vel = ((self.x_vel_value * -1) if self.x_vel_sign else self.x_vel_value) - return getattr(self, '_m_x_vel', None) - - @property - def x_accel(self): - if hasattr(self, '_m_x_accel'): - return self._m_x_accel - - self._m_x_accel = ((self.x_accel_value * -1) if self.x_accel_sign else self.x_accel_value) - return getattr(self, '_m_x_accel', None) - - @property - def x(self): - if hasattr(self, '_m_x'): - return self._m_x - - self._m_x = ((self.x_value * -1) if self.x_sign else self.x_value) - return getattr(self, '_m_x', None) - - - class String2(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.b_n = self._io.read_bits_int_be(3) - self.p2 = self._io.read_bits_int_be(1) != 0 - self.t_b = self._io.read_bits_int_be(7) - self.not_used = self._io.read_bits_int_be(5) - self.y_vel_sign = self._io.read_bits_int_be(1) != 0 - self.y_vel_value = self._io.read_bits_int_be(23) - self.y_accel_sign = self._io.read_bits_int_be(1) != 0 - self.y_accel_value = self._io.read_bits_int_be(4) - self.y_sign = self._io.read_bits_int_be(1) != 0 - self.y_value = self._io.read_bits_int_be(26) - - @property - def y_vel(self): - if hasattr(self, '_m_y_vel'): - return self._m_y_vel - - self._m_y_vel = ((self.y_vel_value * -1) if self.y_vel_sign else self.y_vel_value) - return getattr(self, '_m_y_vel', None) - - @property - def y_accel(self): - if hasattr(self, '_m_y_accel'): - return self._m_y_accel - - self._m_y_accel = ((self.y_accel_value * -1) if self.y_accel_sign else self.y_accel_value) - return getattr(self, '_m_y_accel', None) - - @property - def y(self): - if hasattr(self, '_m_y'): - return self._m_y - - self._m_y = ((self.y_value * -1) if self.y_sign else self.y_value) - return getattr(self, '_m_y', None) - - - class String3(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.p3 = self._io.read_bits_int_be(1) != 0 - self.gamma_n_sign = self._io.read_bits_int_be(1) != 0 - self.gamma_n_value = self._io.read_bits_int_be(10) - self.not_used = self._io.read_bits_int_be(1) != 0 - self.p = self._io.read_bits_int_be(2) - self.l_n = self._io.read_bits_int_be(1) != 0 - self.z_vel_sign = self._io.read_bits_int_be(1) != 0 - self.z_vel_value = self._io.read_bits_int_be(23) - self.z_accel_sign = self._io.read_bits_int_be(1) != 0 - self.z_accel_value = self._io.read_bits_int_be(4) - self.z_sign = self._io.read_bits_int_be(1) != 0 - self.z_value = self._io.read_bits_int_be(26) - - @property - def gamma_n(self): - if hasattr(self, '_m_gamma_n'): - return self._m_gamma_n - - self._m_gamma_n = ((self.gamma_n_value * -1) if self.gamma_n_sign else self.gamma_n_value) - return getattr(self, '_m_gamma_n', None) - - @property - def z_vel(self): - if hasattr(self, '_m_z_vel'): - return self._m_z_vel - - self._m_z_vel = ((self.z_vel_value * -1) if self.z_vel_sign else self.z_vel_value) - return getattr(self, '_m_z_vel', None) - - @property - def z_accel(self): - if hasattr(self, '_m_z_accel'): - return self._m_z_accel - - self._m_z_accel = ((self.z_accel_value * -1) if self.z_accel_sign else self.z_accel_value) - return getattr(self, '_m_z_accel', None) - - @property - def z(self): - if hasattr(self, '_m_z'): - return self._m_z - - self._m_z = ((self.z_value * -1) if self.z_sign else self.z_value) - return getattr(self, '_m_z', None) - - diff --git a/system/ubloxd/generated/gps.py b/system/ubloxd/generated/gps.py deleted file mode 100644 index a999016f3e..0000000000 --- a/system/ubloxd/generated/gps.py +++ /dev/null @@ -1,193 +0,0 @@ -# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -import kaitaistruct -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO - - -if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): - raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) - -class Gps(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.tlm = Gps.Tlm(self._io, self, self._root) - self.how = Gps.How(self._io, self, self._root) - _on = self.how.subframe_id - if _on == 1: - self.body = Gps.Subframe1(self._io, self, self._root) - elif _on == 2: - self.body = Gps.Subframe2(self._io, self, self._root) - elif _on == 3: - self.body = Gps.Subframe3(self._io, self, self._root) - elif _on == 4: - self.body = Gps.Subframe4(self._io, self, self._root) - - class Subframe1(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.week_no = self._io.read_bits_int_be(10) - self.code = self._io.read_bits_int_be(2) - self.sv_accuracy = self._io.read_bits_int_be(4) - self.sv_health = self._io.read_bits_int_be(6) - self.iodc_msb = self._io.read_bits_int_be(2) - self.l2_p_data_flag = self._io.read_bits_int_be(1) != 0 - self.reserved1 = self._io.read_bits_int_be(23) - self.reserved2 = self._io.read_bits_int_be(24) - self.reserved3 = self._io.read_bits_int_be(24) - self.reserved4 = self._io.read_bits_int_be(16) - self._io.align_to_byte() - self.t_gd = self._io.read_s1() - self.iodc_lsb = self._io.read_u1() - self.t_oc = self._io.read_u2be() - self.af_2 = self._io.read_s1() - self.af_1 = self._io.read_s2be() - self.af_0_sign = self._io.read_bits_int_be(1) != 0 - self.af_0_value = self._io.read_bits_int_be(21) - self.reserved5 = self._io.read_bits_int_be(2) - - @property - def af_0(self): - if hasattr(self, '_m_af_0'): - return self._m_af_0 - - self._m_af_0 = ((self.af_0_value - (1 << 21)) if self.af_0_sign else self.af_0_value) - return getattr(self, '_m_af_0', None) - - - class Subframe3(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.c_ic = self._io.read_s2be() - self.omega_0 = self._io.read_s4be() - self.c_is = self._io.read_s2be() - self.i_0 = self._io.read_s4be() - self.c_rc = self._io.read_s2be() - self.omega = self._io.read_s4be() - self.omega_dot_sign = self._io.read_bits_int_be(1) != 0 - self.omega_dot_value = self._io.read_bits_int_be(23) - self._io.align_to_byte() - self.iode = self._io.read_u1() - self.idot_sign = self._io.read_bits_int_be(1) != 0 - self.idot_value = self._io.read_bits_int_be(13) - self.reserved = self._io.read_bits_int_be(2) - - @property - def omega_dot(self): - if hasattr(self, '_m_omega_dot'): - return self._m_omega_dot - - self._m_omega_dot = ((self.omega_dot_value - (1 << 23)) if self.omega_dot_sign else self.omega_dot_value) - return getattr(self, '_m_omega_dot', None) - - @property - def idot(self): - if hasattr(self, '_m_idot'): - return self._m_idot - - self._m_idot = ((self.idot_value - (1 << 13)) if self.idot_sign else self.idot_value) - return getattr(self, '_m_idot', None) - - - class Subframe4(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.data_id = self._io.read_bits_int_be(2) - self.page_id = self._io.read_bits_int_be(6) - self._io.align_to_byte() - _on = self.page_id - if _on == 56: - self.body = Gps.Subframe4.IonosphereData(self._io, self, self._root) - - class IonosphereData(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.a0 = self._io.read_s1() - self.a1 = self._io.read_s1() - self.a2 = self._io.read_s1() - self.a3 = self._io.read_s1() - self.b0 = self._io.read_s1() - self.b1 = self._io.read_s1() - self.b2 = self._io.read_s1() - self.b3 = self._io.read_s1() - - - - class How(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.tow_count = self._io.read_bits_int_be(17) - self.alert = self._io.read_bits_int_be(1) != 0 - self.anti_spoof = self._io.read_bits_int_be(1) != 0 - self.subframe_id = self._io.read_bits_int_be(3) - self.reserved = self._io.read_bits_int_be(2) - - - class Tlm(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.preamble = self._io.read_bytes(1) - if not self.preamble == b"\x8B": - raise kaitaistruct.ValidationNotEqualError(b"\x8B", self.preamble, self._io, u"/types/tlm/seq/0") - self.tlm = self._io.read_bits_int_be(14) - self.integrity_status = self._io.read_bits_int_be(1) != 0 - self.reserved = self._io.read_bits_int_be(1) != 0 - - - class Subframe2(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.iode = self._io.read_u1() - self.c_rs = self._io.read_s2be() - self.delta_n = self._io.read_s2be() - self.m_0 = self._io.read_s4be() - self.c_uc = self._io.read_s2be() - self.e = self._io.read_s4be() - self.c_us = self._io.read_s2be() - self.sqrt_a = self._io.read_u4be() - self.t_oe = self._io.read_u2be() - self.fit_interval_flag = self._io.read_bits_int_be(1) != 0 - self.aoda = self._io.read_bits_int_be(5) - self.reserved = self._io.read_bits_int_be(2) - - - diff --git a/system/ubloxd/generated/ubx.py b/system/ubloxd/generated/ubx.py deleted file mode 100644 index 9946584388..0000000000 --- a/system/ubloxd/generated/ubx.py +++ /dev/null @@ -1,273 +0,0 @@ -# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -import kaitaistruct -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO -from enum import Enum - - -if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): - raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) - -class Ubx(KaitaiStruct): - - class GnssType(Enum): - gps = 0 - sbas = 1 - galileo = 2 - beidou = 3 - imes = 4 - qzss = 5 - glonass = 6 - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.magic = self._io.read_bytes(2) - if not self.magic == b"\xB5\x62": - raise kaitaistruct.ValidationNotEqualError(b"\xB5\x62", self.magic, self._io, u"/seq/0") - self.msg_type = self._io.read_u2be() - self.length = self._io.read_u2le() - _on = self.msg_type - if _on == 2569: - self.body = Ubx.MonHw(self._io, self, self._root) - elif _on == 533: - self.body = Ubx.RxmRawx(self._io, self, self._root) - elif _on == 531: - self.body = Ubx.RxmSfrbx(self._io, self, self._root) - elif _on == 309: - self.body = Ubx.NavSat(self._io, self, self._root) - elif _on == 2571: - self.body = Ubx.MonHw2(self._io, self, self._root) - elif _on == 263: - self.body = Ubx.NavPvt(self._io, self, self._root) - - class RxmRawx(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.rcv_tow = self._io.read_f8le() - self.week = self._io.read_u2le() - self.leap_s = self._io.read_s1() - self.num_meas = self._io.read_u1() - self.rec_stat = self._io.read_u1() - self.reserved1 = self._io.read_bytes(3) - self._raw_meas = [] - self.meas = [] - for i in range(self.num_meas): - self._raw_meas.append(self._io.read_bytes(32)) - _io__raw_meas = KaitaiStream(BytesIO(self._raw_meas[i])) - self.meas.append(Ubx.RxmRawx.Measurement(_io__raw_meas, self, self._root)) - - - class Measurement(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.pr_mes = self._io.read_f8le() - self.cp_mes = self._io.read_f8le() - self.do_mes = self._io.read_f4le() - self.gnss_id = KaitaiStream.resolve_enum(Ubx.GnssType, self._io.read_u1()) - self.sv_id = self._io.read_u1() - self.reserved2 = self._io.read_bytes(1) - self.freq_id = self._io.read_u1() - self.lock_time = self._io.read_u2le() - self.cno = self._io.read_u1() - self.pr_stdev = self._io.read_u1() - self.cp_stdev = self._io.read_u1() - self.do_stdev = self._io.read_u1() - self.trk_stat = self._io.read_u1() - self.reserved3 = self._io.read_bytes(1) - - - - class RxmSfrbx(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.gnss_id = KaitaiStream.resolve_enum(Ubx.GnssType, self._io.read_u1()) - self.sv_id = self._io.read_u1() - self.reserved1 = self._io.read_bytes(1) - self.freq_id = self._io.read_u1() - self.num_words = self._io.read_u1() - self.reserved2 = self._io.read_bytes(1) - self.version = self._io.read_u1() - self.reserved3 = self._io.read_bytes(1) - self.body = [] - for i in range(self.num_words): - self.body.append(self._io.read_u4le()) - - - - class NavSat(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.itow = self._io.read_u4le() - self.version = self._io.read_u1() - self.num_svs = self._io.read_u1() - self.reserved = self._io.read_bytes(2) - self._raw_svs = [] - self.svs = [] - for i in range(self.num_svs): - self._raw_svs.append(self._io.read_bytes(12)) - _io__raw_svs = KaitaiStream(BytesIO(self._raw_svs[i])) - self.svs.append(Ubx.NavSat.Nav(_io__raw_svs, self, self._root)) - - - class Nav(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.gnss_id = KaitaiStream.resolve_enum(Ubx.GnssType, self._io.read_u1()) - self.sv_id = self._io.read_u1() - self.cno = self._io.read_u1() - self.elev = self._io.read_s1() - self.azim = self._io.read_s2le() - self.pr_res = self._io.read_s2le() - self.flags = self._io.read_u4le() - - - - class NavPvt(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.i_tow = self._io.read_u4le() - self.year = self._io.read_u2le() - self.month = self._io.read_u1() - self.day = self._io.read_u1() - self.hour = self._io.read_u1() - self.min = self._io.read_u1() - self.sec = self._io.read_u1() - self.valid = self._io.read_u1() - self.t_acc = self._io.read_u4le() - self.nano = self._io.read_s4le() - self.fix_type = self._io.read_u1() - self.flags = self._io.read_u1() - self.flags2 = self._io.read_u1() - self.num_sv = self._io.read_u1() - self.lon = self._io.read_s4le() - self.lat = self._io.read_s4le() - self.height = self._io.read_s4le() - self.h_msl = self._io.read_s4le() - self.h_acc = self._io.read_u4le() - self.v_acc = self._io.read_u4le() - self.vel_n = self._io.read_s4le() - self.vel_e = self._io.read_s4le() - self.vel_d = self._io.read_s4le() - self.g_speed = self._io.read_s4le() - self.head_mot = self._io.read_s4le() - self.s_acc = self._io.read_s4le() - self.head_acc = self._io.read_u4le() - self.p_dop = self._io.read_u2le() - self.flags3 = self._io.read_u1() - self.reserved1 = self._io.read_bytes(5) - self.head_veh = self._io.read_s4le() - self.mag_dec = self._io.read_s2le() - self.mag_acc = self._io.read_u2le() - - - class MonHw2(KaitaiStruct): - - class ConfigSource(Enum): - flash = 102 - otp = 111 - config_pins = 112 - rom = 113 - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.ofs_i = self._io.read_s1() - self.mag_i = self._io.read_u1() - self.ofs_q = self._io.read_s1() - self.mag_q = self._io.read_u1() - self.cfg_source = KaitaiStream.resolve_enum(Ubx.MonHw2.ConfigSource, self._io.read_u1()) - self.reserved1 = self._io.read_bytes(3) - self.low_lev_cfg = self._io.read_u4le() - self.reserved2 = self._io.read_bytes(8) - self.post_status = self._io.read_u4le() - self.reserved3 = self._io.read_bytes(4) - - - class MonHw(KaitaiStruct): - - class AntennaStatus(Enum): - init = 0 - dontknow = 1 - ok = 2 - short = 3 - open = 4 - - class AntennaPower(Enum): - false = 0 - true = 1 - dontknow = 2 - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.pin_sel = self._io.read_u4le() - self.pin_bank = self._io.read_u4le() - self.pin_dir = self._io.read_u4le() - self.pin_val = self._io.read_u4le() - self.noise_per_ms = self._io.read_u2le() - self.agc_cnt = self._io.read_u2le() - self.a_status = KaitaiStream.resolve_enum(Ubx.MonHw.AntennaStatus, self._io.read_u1()) - self.a_power = KaitaiStream.resolve_enum(Ubx.MonHw.AntennaPower, self._io.read_u1()) - self.flags = self._io.read_u1() - self.reserved1 = self._io.read_bytes(1) - self.used_mask = self._io.read_u4le() - self.vp = self._io.read_bytes(17) - self.jam_ind = self._io.read_u1() - self.reserved2 = self._io.read_bytes(2) - self.pin_irq = self._io.read_u4le() - self.pull_h = self._io.read_u4le() - self.pull_l = self._io.read_u4le() - - - @property - def checksum(self): - if hasattr(self, '_m_checksum'): - return self._m_checksum - - _pos = self._io.pos() - self._io.seek((self.length + 6)) - self._m_checksum = self._io.read_u2le() - self._io.seek(_pos) - return getattr(self, '_m_checksum', None) - - diff --git a/system/ubloxd/glonass.ksy b/system/ubloxd/glonass.ksy deleted file mode 100644 index be99f6e497..0000000000 --- a/system/ubloxd/glonass.ksy +++ /dev/null @@ -1,176 +0,0 @@ -# http://gauss.gge.unb.ca/GLONASS.ICD.pdf -# some variables are misprinted but good in the old doc -# https://www.unavco.org/help/glossary/docs/ICD_GLONASS_4.0_(1998)_en.pdf -meta: - id: glonass - endian: be - bit-endian: be -seq: - - id: idle_chip - type: b1 - - id: string_number - type: b4 - - id: data - type: - switch-on: string_number - cases: - 1: string_1 - 2: string_2 - 3: string_3 - 4: string_4 - 5: string_5 - _: string_non_immediate - - id: hamming_code - type: b8 - - id: pad_1 - type: b11 - - id: superframe_number - type: b16 - - id: pad_2 - type: b8 - - id: frame_number - type: b8 - -types: - string_1: - seq: - - id: not_used - type: b2 - - id: p1 - type: b2 - - id: t_k - type: b12 - - id: x_vel_sign - type: b1 - - id: x_vel_value - type: b23 - - id: x_accel_sign - type: b1 - - id: x_accel_value - type: b4 - - id: x_sign - type: b1 - - id: x_value - type: b26 - instances: - x_vel: - value: 'x_vel_sign ? (x_vel_value * (-1)) : x_vel_value' - x_accel: - value: 'x_accel_sign ? (x_accel_value * (-1)) : x_accel_value' - x: - value: 'x_sign ? (x_value * (-1)) : x_value' - string_2: - seq: - - id: b_n - type: b3 - - id: p2 - type: b1 - - id: t_b - type: b7 - - id: not_used - type: b5 - - id: y_vel_sign - type: b1 - - id: y_vel_value - type: b23 - - id: y_accel_sign - type: b1 - - id: y_accel_value - type: b4 - - id: y_sign - type: b1 - - id: y_value - type: b26 - instances: - y_vel: - value: 'y_vel_sign ? (y_vel_value * (-1)) : y_vel_value' - y_accel: - value: 'y_accel_sign ? (y_accel_value * (-1)) : y_accel_value' - y: - value: 'y_sign ? (y_value * (-1)) : y_value' - string_3: - seq: - - id: p3 - type: b1 - - id: gamma_n_sign - type: b1 - - id: gamma_n_value - type: b10 - - id: not_used - type: b1 - - id: p - type: b2 - - id: l_n - type: b1 - - id: z_vel_sign - type: b1 - - id: z_vel_value - type: b23 - - id: z_accel_sign - type: b1 - - id: z_accel_value - type: b4 - - id: z_sign - type: b1 - - id: z_value - type: b26 - instances: - gamma_n: - value: 'gamma_n_sign ? (gamma_n_value * (-1)) : gamma_n_value' - z_vel: - value: 'z_vel_sign ? (z_vel_value * (-1)) : z_vel_value' - z_accel: - value: 'z_accel_sign ? (z_accel_value * (-1)) : z_accel_value' - z: - value: 'z_sign ? (z_value * (-1)) : z_value' - string_4: - seq: - - id: tau_n_sign - type: b1 - - id: tau_n_value - type: b21 - - id: delta_tau_n_sign - type: b1 - - id: delta_tau_n_value - type: b4 - - id: e_n - type: b5 - - id: not_used_1 - type: b14 - - id: p4 - type: b1 - - id: f_t - type: b4 - - id: not_used_2 - type: b3 - - id: n_t - type: b11 - - id: n - type: b5 - - id: m - type: b2 - instances: - tau_n: - value: 'tau_n_sign ? (tau_n_value * (-1)) : tau_n_value' - delta_tau_n: - value: 'delta_tau_n_sign ? (delta_tau_n_value * (-1)) : delta_tau_n_value' - string_5: - seq: - - id: n_a - type: b11 - - id: tau_c - type: b32 - - id: not_used - type: b1 - - id: n_4 - type: b5 - - id: tau_gps - type: b22 - - id: l_n - type: b1 - string_non_immediate: - seq: - - id: data_1 - type: b64 - - id: data_2 - type: b8 diff --git a/system/ubloxd/glonass.py b/system/ubloxd/glonass.py new file mode 100644 index 0000000000..144ccdde6e --- /dev/null +++ b/system/ubloxd/glonass.py @@ -0,0 +1,156 @@ +""" +Parses GLONASS navigation strings per GLONASS ICD specification. +http://gauss.gge.unb.ca/GLONASS.ICD.pdf +https://www.unavco.org/help/glossary/docs/ICD_GLONASS_4.0_(1998)_en.pdf +""" + +from typing import Annotated + +from openpilot.system.ubloxd import binary_struct as bs + + +class Glonass(bs.BinaryStruct): + class String1(bs.BinaryStruct): + not_used: Annotated[int, bs.bits(2)] + p1: Annotated[int, bs.bits(2)] + t_k: Annotated[int, bs.bits(12)] + x_vel_sign: Annotated[bool, bs.bits(1)] + x_vel_value: Annotated[int, bs.bits(23)] + x_accel_sign: Annotated[bool, bs.bits(1)] + x_accel_value: Annotated[int, bs.bits(4)] + x_sign: Annotated[bool, bs.bits(1)] + x_value: Annotated[int, bs.bits(26)] + + @property + def x_vel(self) -> int: + """Computed x_vel from sign-magnitude representation.""" + return (self.x_vel_value * -1) if self.x_vel_sign else self.x_vel_value + + @property + def x_accel(self) -> int: + """Computed x_accel from sign-magnitude representation.""" + return (self.x_accel_value * -1) if self.x_accel_sign else self.x_accel_value + + @property + def x(self) -> int: + """Computed x from sign-magnitude representation.""" + return (self.x_value * -1) if self.x_sign else self.x_value + + class String2(bs.BinaryStruct): + b_n: Annotated[int, bs.bits(3)] + p2: Annotated[bool, bs.bits(1)] + t_b: Annotated[int, bs.bits(7)] + not_used: Annotated[int, bs.bits(5)] + y_vel_sign: Annotated[bool, bs.bits(1)] + y_vel_value: Annotated[int, bs.bits(23)] + y_accel_sign: Annotated[bool, bs.bits(1)] + y_accel_value: Annotated[int, bs.bits(4)] + y_sign: Annotated[bool, bs.bits(1)] + y_value: Annotated[int, bs.bits(26)] + + @property + def y_vel(self) -> int: + """Computed y_vel from sign-magnitude representation.""" + return (self.y_vel_value * -1) if self.y_vel_sign else self.y_vel_value + + @property + def y_accel(self) -> int: + """Computed y_accel from sign-magnitude representation.""" + return (self.y_accel_value * -1) if self.y_accel_sign else self.y_accel_value + + @property + def y(self) -> int: + """Computed y from sign-magnitude representation.""" + return (self.y_value * -1) if self.y_sign else self.y_value + + class String3(bs.BinaryStruct): + p3: Annotated[bool, bs.bits(1)] + gamma_n_sign: Annotated[bool, bs.bits(1)] + gamma_n_value: Annotated[int, bs.bits(10)] + not_used: Annotated[bool, bs.bits(1)] + p: Annotated[int, bs.bits(2)] + l_n: Annotated[bool, bs.bits(1)] + z_vel_sign: Annotated[bool, bs.bits(1)] + z_vel_value: Annotated[int, bs.bits(23)] + z_accel_sign: Annotated[bool, bs.bits(1)] + z_accel_value: Annotated[int, bs.bits(4)] + z_sign: Annotated[bool, bs.bits(1)] + z_value: Annotated[int, bs.bits(26)] + + @property + def gamma_n(self) -> int: + """Computed gamma_n from sign-magnitude representation.""" + return (self.gamma_n_value * -1) if self.gamma_n_sign else self.gamma_n_value + + @property + def z_vel(self) -> int: + """Computed z_vel from sign-magnitude representation.""" + return (self.z_vel_value * -1) if self.z_vel_sign else self.z_vel_value + + @property + def z_accel(self) -> int: + """Computed z_accel from sign-magnitude representation.""" + return (self.z_accel_value * -1) if self.z_accel_sign else self.z_accel_value + + @property + def z(self) -> int: + """Computed z from sign-magnitude representation.""" + return (self.z_value * -1) if self.z_sign else self.z_value + + class String4(bs.BinaryStruct): + tau_n_sign: Annotated[bool, bs.bits(1)] + tau_n_value: Annotated[int, bs.bits(21)] + delta_tau_n_sign: Annotated[bool, bs.bits(1)] + delta_tau_n_value: Annotated[int, bs.bits(4)] + e_n: Annotated[int, bs.bits(5)] + not_used_1: Annotated[int, bs.bits(14)] + p4: Annotated[bool, bs.bits(1)] + f_t: Annotated[int, bs.bits(4)] + not_used_2: Annotated[int, bs.bits(3)] + n_t: Annotated[int, bs.bits(11)] + n: Annotated[int, bs.bits(5)] + m: Annotated[int, bs.bits(2)] + + @property + def tau_n(self) -> int: + """Computed tau_n from sign-magnitude representation.""" + return (self.tau_n_value * -1) if self.tau_n_sign else self.tau_n_value + + @property + def delta_tau_n(self) -> int: + """Computed delta_tau_n from sign-magnitude representation.""" + return (self.delta_tau_n_value * -1) if self.delta_tau_n_sign else self.delta_tau_n_value + + class String5(bs.BinaryStruct): + n_a: Annotated[int, bs.bits(11)] + tau_c: Annotated[int, bs.bits(32)] + not_used: Annotated[bool, bs.bits(1)] + n_4: Annotated[int, bs.bits(5)] + tau_gps: Annotated[int, bs.bits(22)] + l_n: Annotated[bool, bs.bits(1)] + + class StringNonImmediate(bs.BinaryStruct): + data_1: Annotated[int, bs.bits(64)] + data_2: Annotated[int, bs.bits(8)] + + idle_chip: Annotated[bool, bs.bits(1)] + string_number: Annotated[int, bs.bits(4)] + data: Annotated[ + object, + bs.switch( + 'string_number', + { + 1: String1, + 2: String2, + 3: String3, + 4: String4, + 5: String5, + }, + default=StringNonImmediate, + ), + ] + hamming_code: Annotated[int, bs.bits(8)] + pad_1: Annotated[int, bs.bits(11)] + superframe_number: Annotated[int, bs.bits(16)] + pad_2: Annotated[int, bs.bits(8)] + frame_number: Annotated[int, bs.bits(8)] diff --git a/system/ubloxd/gps.ksy b/system/ubloxd/gps.ksy deleted file mode 100644 index 893ad1b25b..0000000000 --- a/system/ubloxd/gps.ksy +++ /dev/null @@ -1,189 +0,0 @@ -# https://www.gps.gov/technical/icwg/IS-GPS-200E.pdf -meta: - id: gps - endian: be - bit-endian: be -seq: - - id: tlm - type: tlm - - id: how - type: how - - id: body - type: - switch-on: how.subframe_id - cases: - 1: subframe_1 - 2: subframe_2 - 3: subframe_3 - 4: subframe_4 -types: - tlm: - seq: - - id: preamble - contents: [0x8b] - - id: tlm - type: b14 - - id: integrity_status - type: b1 - - id: reserved - type: b1 - how: - seq: - - id: tow_count - type: b17 - - id: alert - type: b1 - - id: anti_spoof - type: b1 - - id: subframe_id - type: b3 - - id: reserved - type: b2 - subframe_1: - seq: - # Word 3 - - id: week_no - type: b10 - - id: code - type: b2 - - id: sv_accuracy - type: b4 - - id: sv_health - type: b6 - - id: iodc_msb - type: b2 - # Word 4 - - id: l2_p_data_flag - type: b1 - - id: reserved1 - type: b23 - # Word 5 - - id: reserved2 - type: b24 - # Word 6 - - id: reserved3 - type: b24 - # Word 7 - - id: reserved4 - type: b16 - - id: t_gd - type: s1 - # Word 8 - - id: iodc_lsb - type: u1 - - id: t_oc - type: u2 - # Word 9 - - id: af_2 - type: s1 - - id: af_1 - type: s2 - # Word 10 - - id: af_0_sign - type: b1 - - id: af_0_value - type: b21 - - id: reserved5 - type: b2 - instances: - af_0: - value: 'af_0_sign ? (af_0_value - (1 << 21)) : af_0_value' - subframe_2: - seq: - # Word 3 - - id: iode - type: u1 - - id: c_rs - type: s2 - # Word 4 & 5 - - id: delta_n - type: s2 - - id: m_0 - type: s4 - # Word 6 & 7 - - id: c_uc - type: s2 - - id: e - type: s4 - # Word 8 & 9 - - id: c_us - type: s2 - - id: sqrt_a - type: u4 - # Word 10 - - id: t_oe - type: u2 - - id: fit_interval_flag - type: b1 - - id: aoda - type: b5 - - id: reserved - type: b2 - subframe_3: - seq: - # Word 3 & 4 - - id: c_ic - type: s2 - - id: omega_0 - type: s4 - # Word 5 & 6 - - id: c_is - type: s2 - - id: i_0 - type: s4 - # Word 7 & 8 - - id: c_rc - type: s2 - - id: omega - type: s4 - # Word 9 - - id: omega_dot_sign - type: b1 - - id: omega_dot_value - type: b23 - # Word 10 - - id: iode - type: u1 - - id: idot_sign - type: b1 - - id: idot_value - type: b13 - - id: reserved - type: b2 - instances: - omega_dot: - value: 'omega_dot_sign ? (omega_dot_value - (1 << 23)) : omega_dot_value' - idot: - value: 'idot_sign ? (idot_value - (1 << 13)) : idot_value' - subframe_4: - seq: - # Word 3 - - id: data_id - type: b2 - - id: page_id - type: b6 - - id: body - type: - switch-on: page_id - cases: - 56: ionosphere_data - types: - ionosphere_data: - seq: - - id: a0 - type: s1 - - id: a1 - type: s1 - - id: a2 - type: s1 - - id: a3 - type: s1 - - id: b0 - type: s1 - - id: b1 - type: s1 - - id: b2 - type: s1 - - id: b3 - type: s1 - diff --git a/system/ubloxd/gps.py b/system/ubloxd/gps.py new file mode 100644 index 0000000000..1c0833bd92 --- /dev/null +++ b/system/ubloxd/gps.py @@ -0,0 +1,116 @@ +""" +Parses GPS navigation subframes per IS-GPS-200E specification. +https://www.gps.gov/technical/icwg/IS-GPS-200E.pdf +""" + +from typing import Annotated + +from openpilot.system.ubloxd import binary_struct as bs + + +class Gps(bs.BinaryStruct): + class Tlm(bs.BinaryStruct): + preamble: Annotated[bytes, bs.const(bs.bytes_field(1), b"\x8b")] + tlm: Annotated[int, bs.bits(14)] + integrity_status: Annotated[bool, bs.bits(1)] + reserved: Annotated[bool, bs.bits(1)] + + class How(bs.BinaryStruct): + tow_count: Annotated[int, bs.bits(17)] + alert: Annotated[bool, bs.bits(1)] + anti_spoof: Annotated[bool, bs.bits(1)] + subframe_id: Annotated[int, bs.bits(3)] + reserved: Annotated[int, bs.bits(2)] + + class Subframe1(bs.BinaryStruct): + week_no: Annotated[int, bs.bits(10)] + code: Annotated[int, bs.bits(2)] + sv_accuracy: Annotated[int, bs.bits(4)] + sv_health: Annotated[int, bs.bits(6)] + iodc_msb: Annotated[int, bs.bits(2)] + l2_p_data_flag: Annotated[bool, bs.bits(1)] + reserved1: Annotated[int, bs.bits(23)] + reserved2: Annotated[int, bs.bits(24)] + reserved3: Annotated[int, bs.bits(24)] + reserved4: Annotated[int, bs.bits(16)] + t_gd: Annotated[int, bs.s8] + iodc_lsb: Annotated[int, bs.u8] + t_oc: Annotated[int, bs.u16be] + af_2: Annotated[int, bs.s8] + af_1: Annotated[int, bs.s16be] + af_0_sign: Annotated[bool, bs.bits(1)] + af_0_value: Annotated[int, bs.bits(21)] + reserved5: Annotated[int, bs.bits(2)] + + @property + def af_0(self) -> int: + """Computed af_0 from sign-magnitude representation.""" + return (self.af_0_value - (1 << 21)) if self.af_0_sign else self.af_0_value + + class Subframe2(bs.BinaryStruct): + iode: Annotated[int, bs.u8] + c_rs: Annotated[int, bs.s16be] + delta_n: Annotated[int, bs.s16be] + m_0: Annotated[int, bs.s32be] + c_uc: Annotated[int, bs.s16be] + e: Annotated[int, bs.s32be] + c_us: Annotated[int, bs.s16be] + sqrt_a: Annotated[int, bs.u32be] + t_oe: Annotated[int, bs.u16be] + fit_interval_flag: Annotated[bool, bs.bits(1)] + aoda: Annotated[int, bs.bits(5)] + reserved: Annotated[int, bs.bits(2)] + + class Subframe3(bs.BinaryStruct): + c_ic: Annotated[int, bs.s16be] + omega_0: Annotated[int, bs.s32be] + c_is: Annotated[int, bs.s16be] + i_0: Annotated[int, bs.s32be] + c_rc: Annotated[int, bs.s16be] + omega: Annotated[int, bs.s32be] + omega_dot_sign: Annotated[bool, bs.bits(1)] + omega_dot_value: Annotated[int, bs.bits(23)] + iode: Annotated[int, bs.u8] + idot_sign: Annotated[bool, bs.bits(1)] + idot_value: Annotated[int, bs.bits(13)] + reserved: Annotated[int, bs.bits(2)] + + @property + def omega_dot(self) -> int: + """Computed omega_dot from sign-magnitude representation.""" + return (self.omega_dot_value - (1 << 23)) if self.omega_dot_sign else self.omega_dot_value + + @property + def idot(self) -> int: + """Computed idot from sign-magnitude representation.""" + return (self.idot_value - (1 << 13)) if self.idot_sign else self.idot_value + + class Subframe4(bs.BinaryStruct): + class IonosphereData(bs.BinaryStruct): + a0: Annotated[int, bs.s8] + a1: Annotated[int, bs.s8] + a2: Annotated[int, bs.s8] + a3: Annotated[int, bs.s8] + b0: Annotated[int, bs.s8] + b1: Annotated[int, bs.s8] + b2: Annotated[int, bs.s8] + b3: Annotated[int, bs.s8] + + data_id: Annotated[int, bs.bits(2)] + page_id: Annotated[int, bs.bits(6)] + body: Annotated[object, bs.switch('page_id', {56: IonosphereData})] + + tlm: Tlm + how: How + body: Annotated[ + object, + bs.switch( + 'how.subframe_id', + { + 1: Subframe1, + 2: Subframe2, + 3: Subframe3, + 4: Subframe4, + }, + ), + ] diff --git a/system/ubloxd/ubloxd.py b/system/ubloxd/ubloxd.py index 6882ad0955..e55cadcf78 100755 --- a/system/ubloxd/ubloxd.py +++ b/system/ubloxd/ubloxd.py @@ -8,9 +8,9 @@ from dataclasses import dataclass from cereal import log from cereal import messaging -from openpilot.system.ubloxd.generated.ubx import Ubx -from openpilot.system.ubloxd.generated.gps import Gps -from openpilot.system.ubloxd.generated.glonass import Glonass +from openpilot.system.ubloxd.ubx import Ubx +from openpilot.system.ubloxd.gps import Gps +from openpilot.system.ubloxd.glonass import Glonass SECS_IN_MIN = 60 @@ -52,7 +52,7 @@ class UbxFramer: # find preamble if len(self.buf) < 2: break - start = self.buf.find(b"\xB5\x62") + start = self.buf.find(b"\xb5\x62") if start < 0: # no preamble in buffer self.buf.clear() @@ -98,9 +98,22 @@ class UbloxMsgParser: # user range accuracy in meters glonass_URA_lookup: dict[int, float] = { - 0: 1, 1: 2, 2: 2.5, 3: 4, 4: 5, 5: 7, - 6: 10, 7: 12, 8: 14, 9: 16, 10: 32, - 11: 64, 12: 128, 13: 256, 14: 512, 15: 1024, + 0: 1, + 1: 2, + 2: 2.5, + 3: 4, + 4: 5, + 5: 7, + 6: 10, + 7: 12, + 8: 14, + 9: 16, + 10: 32, + 11: 64, + 12: 128, + 13: 256, + 14: 512, + 15: 1024, } def __init__(self) -> None: @@ -121,7 +134,7 @@ class UbloxMsgParser: body = Ubx.NavPvt.from_bytes(payload) return self._gen_nav_pvt(body) if msg_type == 0x0213: - # Manually parse RXM-SFRBX to avoid Kaitai EOF on some frames + # Manually parse RXM-SFRBX to avoid EOF on some frames if len(payload) < 8: return None gnss_id = payload[0] @@ -134,7 +147,7 @@ class UbloxMsgParser: words: list[int] = [] off = 8 for _ in range(num_words): - words.append(int.from_bytes(payload[off:off+4], 'little')) + words.append(int.from_bytes(payload[off : off + 4], 'little')) off += 4 class _SfrbxView: @@ -143,6 +156,7 @@ class UbloxMsgParser: self.sv_id = sid self.freq_id = fid self.body = body + view = _SfrbxView(gnss_id, sv_id, freq_id, words) return self._gen_rxm_sfrbx(view) if msg_type == 0x0215: @@ -515,5 +529,6 @@ def main(): service, dat = res pm.send(service, dat) + if __name__ == '__main__': main() diff --git a/system/ubloxd/ubx.ksy b/system/ubloxd/ubx.ksy deleted file mode 100644 index 02c757fe71..0000000000 --- a/system/ubloxd/ubx.ksy +++ /dev/null @@ -1,293 +0,0 @@ -meta: - id: ubx - endian: le -seq: - - id: magic - contents: [0xb5, 0x62] - - id: msg_type - type: u2be - - id: length - type: u2 - - id: body - type: - switch-on: msg_type - cases: - 0x0107: nav_pvt - 0x0213: rxm_sfrbx - 0x0215: rxm_rawx - 0x0a09: mon_hw - 0x0a0b: mon_hw2 - 0x0135: nav_sat -instances: - checksum: - pos: length + 6 - type: u2 - -types: - mon_hw: - seq: - - id: pin_sel - type: u4 - - id: pin_bank - type: u4 - - id: pin_dir - type: u4 - - id: pin_val - type: u4 - - id: noise_per_ms - type: u2 - - id: agc_cnt - type: u2 - - id: a_status - type: u1 - enum: antenna_status - - id: a_power - type: u1 - enum: antenna_power - - id: flags - type: u1 - - id: reserved1 - size: 1 - - id: used_mask - type: u4 - - id: vp - size: 17 - - id: jam_ind - type: u1 - - id: reserved2 - size: 2 - - id: pin_irq - type: u4 - - id: pull_h - type: u4 - - id: pull_l - type: u4 - enums: - antenna_status: - 0: init - 1: dontknow - 2: ok - 3: short - 4: open - antenna_power: - 0: off - 1: on - 2: dontknow - - mon_hw2: - seq: - - id: ofs_i - type: s1 - - id: mag_i - type: u1 - - id: ofs_q - type: s1 - - id: mag_q - type: u1 - - id: cfg_source - type: u1 - enum: config_source - - id: reserved1 - size: 3 - - id: low_lev_cfg - type: u4 - - id: reserved2 - size: 8 - - id: post_status - type: u4 - - id: reserved3 - size: 4 - - enums: - config_source: - 113: rom - 111: otp - 112: config_pins - 102: flash - - rxm_sfrbx: - seq: - - id: gnss_id - type: u1 - enum: gnss_type - - id: sv_id - type: u1 - - id: reserved1 - size: 1 - - id: freq_id - type: u1 - - id: num_words - type: u1 - - id: reserved2 - size: 1 - - id: version - type: u1 - - id: reserved3 - size: 1 - - id: body - type: u4 - repeat: expr - repeat-expr: num_words - - rxm_rawx: - seq: - - id: rcv_tow - type: f8 - - id: week - type: u2 - - id: leap_s - type: s1 - - id: num_meas - type: u1 - - id: rec_stat - type: u1 - - id: reserved1 - size: 3 - - id: meas - type: measurement - size: 32 - repeat: expr - repeat-expr: num_meas - types: - measurement: - seq: - - id: pr_mes - type: f8 - - id: cp_mes - type: f8 - - id: do_mes - type: f4 - - id: gnss_id - type: u1 - enum: gnss_type - - id: sv_id - type: u1 - - id: reserved2 - size: 1 - - id: freq_id - type: u1 - - id: lock_time - type: u2 - - id: cno - type: u1 - - id: pr_stdev - type: u1 - - id: cp_stdev - type: u1 - - id: do_stdev - type: u1 - - id: trk_stat - type: u1 - - id: reserved3 - size: 1 - nav_sat: - seq: - - id: itow - type: u4 - - id: version - type: u1 - - id: num_svs - type: u1 - - id: reserved - size: 2 - - id: svs - type: nav - size: 12 - repeat: expr - repeat-expr: num_svs - types: - nav: - seq: - - id: gnss_id - type: u1 - enum: gnss_type - - id: sv_id - type: u1 - - id: cno - type: u1 - - id: elev - type: s1 - - id: azim - type: s2 - - id: pr_res - type: s2 - - id: flags - type: u4 - - nav_pvt: - seq: - - id: i_tow - type: u4 - - id: year - type: u2 - - id: month - type: u1 - - id: day - type: u1 - - id: hour - type: u1 - - id: min - type: u1 - - id: sec - type: u1 - - id: valid - type: u1 - - id: t_acc - type: u4 - - id: nano - type: s4 - - id: fix_type - type: u1 - - id: flags - type: u1 - - id: flags2 - type: u1 - - id: num_sv - type: u1 - - id: lon - type: s4 - - id: lat - type: s4 - - id: height - type: s4 - - id: h_msl - type: s4 - - id: h_acc - type: u4 - - id: v_acc - type: u4 - - id: vel_n - type: s4 - - id: vel_e - type: s4 - - id: vel_d - type: s4 - - id: g_speed - type: s4 - - id: head_mot - type: s4 - - id: s_acc - type: s4 - - id: head_acc - type: u4 - - id: p_dop - type: u2 - - id: flags3 - type: u1 - - id: reserved1 - size: 5 - - id: head_veh - type: s4 - - id: mag_dec - type: s2 - - id: mag_acc - type: u2 -enums: - gnss_type: - 0: gps - 1: sbas - 2: galileo - 3: beidou - 4: imes - 5: qzss - 6: glonass diff --git a/system/ubloxd/ubx.py b/system/ubloxd/ubx.py new file mode 100644 index 0000000000..857498ebf1 --- /dev/null +++ b/system/ubloxd/ubx.py @@ -0,0 +1,180 @@ +""" +UBX protocol parser +""" + +from enum import IntEnum +from typing import Annotated + +from openpilot.system.ubloxd import binary_struct as bs + + +class GnssType(IntEnum): + gps = 0 + sbas = 1 + galileo = 2 + beidou = 3 + imes = 4 + qzss = 5 + glonass = 6 + + +class Ubx(bs.BinaryStruct): + GnssType = GnssType + + class RxmRawx(bs.BinaryStruct): + class Measurement(bs.BinaryStruct): + pr_mes: Annotated[float, bs.f64] + cp_mes: Annotated[float, bs.f64] + do_mes: Annotated[float, bs.f32] + gnss_id: Annotated[GnssType | int, bs.enum(bs.u8, GnssType)] + sv_id: Annotated[int, bs.u8] + reserved2: Annotated[bytes, bs.bytes_field(1)] + freq_id: Annotated[int, bs.u8] + lock_time: Annotated[int, bs.u16] + cno: Annotated[int, bs.u8] + pr_stdev: Annotated[int, bs.u8] + cp_stdev: Annotated[int, bs.u8] + do_stdev: Annotated[int, bs.u8] + trk_stat: Annotated[int, bs.u8] + reserved3: Annotated[bytes, bs.bytes_field(1)] + + rcv_tow: Annotated[float, bs.f64] + week: Annotated[int, bs.u16] + leap_s: Annotated[int, bs.s8] + num_meas: Annotated[int, bs.u8] + rec_stat: Annotated[int, bs.u8] + reserved1: Annotated[bytes, bs.bytes_field(3)] + meas: Annotated[list[Measurement], bs.array(Measurement, count_field='num_meas')] + + class RxmSfrbx(bs.BinaryStruct): + gnss_id: Annotated[GnssType | int, bs.enum(bs.u8, GnssType)] + sv_id: Annotated[int, bs.u8] + reserved1: Annotated[bytes, bs.bytes_field(1)] + freq_id: Annotated[int, bs.u8] + num_words: Annotated[int, bs.u8] + reserved2: Annotated[bytes, bs.bytes_field(1)] + version: Annotated[int, bs.u8] + reserved3: Annotated[bytes, bs.bytes_field(1)] + body: Annotated[list[int], bs.array(bs.u32, count_field='num_words')] + + class NavSat(bs.BinaryStruct): + class Nav(bs.BinaryStruct): + gnss_id: Annotated[GnssType | int, bs.enum(bs.u8, GnssType)] + sv_id: Annotated[int, bs.u8] + cno: Annotated[int, bs.u8] + elev: Annotated[int, bs.s8] + azim: Annotated[int, bs.s16] + pr_res: Annotated[int, bs.s16] + flags: Annotated[int, bs.u32] + + itow: Annotated[int, bs.u32] + version: Annotated[int, bs.u8] + num_svs: Annotated[int, bs.u8] + reserved: Annotated[bytes, bs.bytes_field(2)] + svs: Annotated[list[Nav], bs.array(Nav, count_field='num_svs')] + + class NavPvt(bs.BinaryStruct): + i_tow: Annotated[int, bs.u32] + year: Annotated[int, bs.u16] + month: Annotated[int, bs.u8] + day: Annotated[int, bs.u8] + hour: Annotated[int, bs.u8] + min: Annotated[int, bs.u8] + sec: Annotated[int, bs.u8] + valid: Annotated[int, bs.u8] + t_acc: Annotated[int, bs.u32] + nano: Annotated[int, bs.s32] + fix_type: Annotated[int, bs.u8] + flags: Annotated[int, bs.u8] + flags2: Annotated[int, bs.u8] + num_sv: Annotated[int, bs.u8] + lon: Annotated[int, bs.s32] + lat: Annotated[int, bs.s32] + height: Annotated[int, bs.s32] + h_msl: Annotated[int, bs.s32] + h_acc: Annotated[int, bs.u32] + v_acc: Annotated[int, bs.u32] + vel_n: Annotated[int, bs.s32] + vel_e: Annotated[int, bs.s32] + vel_d: Annotated[int, bs.s32] + g_speed: Annotated[int, bs.s32] + head_mot: Annotated[int, bs.s32] + s_acc: Annotated[int, bs.s32] + head_acc: Annotated[int, bs.u32] + p_dop: Annotated[int, bs.u16] + flags3: Annotated[int, bs.u8] + reserved1: Annotated[bytes, bs.bytes_field(5)] + head_veh: Annotated[int, bs.s32] + mag_dec: Annotated[int, bs.s16] + mag_acc: Annotated[int, bs.u16] + + class MonHw2(bs.BinaryStruct): + class ConfigSource(IntEnum): + flash = 102 + otp = 111 + config_pins = 112 + rom = 113 + + ofs_i: Annotated[int, bs.s8] + mag_i: Annotated[int, bs.u8] + ofs_q: Annotated[int, bs.s8] + mag_q: Annotated[int, bs.u8] + cfg_source: Annotated[ConfigSource | int, bs.enum(bs.u8, ConfigSource)] + reserved1: Annotated[bytes, bs.bytes_field(3)] + low_lev_cfg: Annotated[int, bs.u32] + reserved2: Annotated[bytes, bs.bytes_field(8)] + post_status: Annotated[int, bs.u32] + reserved3: Annotated[bytes, bs.bytes_field(4)] + + class MonHw(bs.BinaryStruct): + class AntennaStatus(IntEnum): + init = 0 + dontknow = 1 + ok = 2 + short = 3 + open = 4 + + class AntennaPower(IntEnum): + false = 0 + true = 1 + dontknow = 2 + + pin_sel: Annotated[int, bs.u32] + pin_bank: Annotated[int, bs.u32] + pin_dir: Annotated[int, bs.u32] + pin_val: Annotated[int, bs.u32] + noise_per_ms: Annotated[int, bs.u16] + agc_cnt: Annotated[int, bs.u16] + a_status: Annotated[AntennaStatus | int, bs.enum(bs.u8, AntennaStatus)] + a_power: Annotated[AntennaPower | int, bs.enum(bs.u8, AntennaPower)] + flags: Annotated[int, bs.u8] + reserved1: Annotated[bytes, bs.bytes_field(1)] + used_mask: Annotated[int, bs.u32] + vp: Annotated[bytes, bs.bytes_field(17)] + jam_ind: Annotated[int, bs.u8] + reserved2: Annotated[bytes, bs.bytes_field(2)] + pin_irq: Annotated[int, bs.u32] + pull_h: Annotated[int, bs.u32] + pull_l: Annotated[int, bs.u32] + + magic: Annotated[bytes, bs.const(bs.bytes_field(2), b"\xb5\x62")] + msg_type: Annotated[int, bs.u16be] + length: Annotated[int, bs.u16] + body: Annotated[ + object, + bs.substream( + 'length', + bs.switch( + 'msg_type', + { + 0x0107: NavPvt, + 0x0213: RxmSfrbx, + 0x0215: RxmRawx, + 0x0A09: MonHw, + 0x0A0B: MonHw2, + 0x0135: NavSat, + }, + ), + ), + ] + checksum: Annotated[int, bs.u16] diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index fcc5ddb727..9e902508b8 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -1,6 +1,7 @@ import atexit import cffi import os +import queue import time import signal import sys @@ -42,6 +43,9 @@ PROFILE_RENDER = int(os.getenv("PROFILE_RENDER", "0")) PROFILE_STATS = int(os.getenv("PROFILE_STATS", "100")) # Number of functions to show in profile output RECORD = os.getenv("RECORD") == "1" RECORD_OUTPUT = str(Path(os.getenv("RECORD_OUTPUT", "output")).with_suffix(".mp4")) +RECORD_BITRATE = os.getenv("RECORD_BITRATE", "") # Target bitrate e.g. "2000k" +RECORD_SPEED = int(os.getenv("RECORD_SPEED", "1")) # Speed multiplier +OFFSCREEN = os.getenv("OFFSCREEN") == "1" # Disable FPS limiting for fast offline rendering GL_VERSION = """ #version 300 es @@ -216,6 +220,9 @@ class GuiApplication(GuiApplicationExt): self._render_texture: rl.RenderTexture | None = None self._burn_in_shader: rl.Shader | None = None self._ffmpeg_proc: subprocess.Popen | None = None + self._ffmpeg_queue: queue.Queue | None = None + self._ffmpeg_thread: threading.Thread | None = None + self._ffmpeg_stop_event: threading.Event | None = None self._textures: dict[str, rl.Texture] = {} self._target_fps: int = _DEFAULT_FPS self._last_fps_log_time: float = time.monotonic() @@ -282,25 +289,36 @@ class GuiApplication(GuiApplicationExt): rl.set_texture_filter(self._render_texture.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) if RECORD: + output_fps = fps * RECORD_SPEED ffmpeg_args = [ 'ffmpeg', '-v', 'warning', # Reduce ffmpeg log spam - '-stats', # Show encoding progress + '-nostats', # Suppress encoding progress '-f', 'rawvideo', # Input format '-pix_fmt', 'rgba', # Input pixel format '-s', f'{self._width}x{self._height}', # Input resolution '-r', str(fps), # Input frame rate '-i', 'pipe:0', # Input from stdin - '-vf', 'vflip,format=yuv420p', # Flip vertically and convert rgba to yuv420p - '-c:v', 'libx264', # Video codec - '-preset', 'ultrafast', # Encoding speed + '-vf', 'vflip,format=yuv420p', # Flip vertically and convert to yuv420p + '-r', str(output_fps), # Output frame rate (for speed multiplier) + '-c:v', 'libx264', + '-preset', 'ultrafast', + ] + if RECORD_BITRATE: + ffmpeg_args += ['-b:v', RECORD_BITRATE, '-maxrate', RECORD_BITRATE, '-bufsize', RECORD_BITRATE] + ffmpeg_args += [ '-y', # Overwrite existing file '-f', 'mp4', # Output format RECORD_OUTPUT, # Output file path ] self._ffmpeg_proc = subprocess.Popen(ffmpeg_args, stdin=subprocess.PIPE) + self._ffmpeg_queue = queue.Queue(maxsize=60) # Buffer up to 60 frames + self._ffmpeg_stop_event = threading.Event() + self._ffmpeg_thread = threading.Thread(target=self._ffmpeg_writer_thread, daemon=True) + self._ffmpeg_thread.start() - rl.set_target_fps(fps) + # OFFSCREEN disables FPS limiting for fast offline rendering (e.g. clips) + rl.set_target_fps(0 if OFFSCREEN else fps) self._target_fps = fps self._set_styles() @@ -342,6 +360,21 @@ class GuiApplication(GuiApplicationExt): print(f"{green}UI window ready in {elapsed_ms:.1f} ms{reset}") sys.exit(0) + def _ffmpeg_writer_thread(self): + """Background thread that writes frames to ffmpeg.""" + while True: + try: + data = self._ffmpeg_queue.get(timeout=1.0) + if data is None: # Sentinel to stop + break + self._ffmpeg_proc.stdin.write(data) + except queue.Empty: + if self._ffmpeg_stop_event.is_set(): + break + continue + except Exception: + break + def set_modal_overlay(self, overlay, callback: Callable | None = None): if self._modal_overlay.overlay is not None: if hasattr(self._modal_overlay.overlay, 'hide_event'): @@ -414,11 +447,17 @@ class GuiApplication(GuiApplicationExt): return texture def close_ffmpeg(self): + if self._ffmpeg_thread is not None: + # Signal thread to stop, send sentinel, then wait for it to drain + self._ffmpeg_stop_event.set() + self._ffmpeg_queue.put(None) + self._ffmpeg_thread.join(timeout=30) + if self._ffmpeg_proc is not None: self._ffmpeg_proc.stdin.flush() self._ffmpeg_proc.stdin.close() try: - self._ffmpeg_proc.wait(timeout=5) + self._ffmpeg_proc.wait(timeout=30) except subprocess.TimeoutExpired: self._ffmpeg_proc.terminate() self._ffmpeg_proc.wait() @@ -533,8 +572,7 @@ class GuiApplication(GuiApplicationExt): image = rl.load_image_from_texture(self._render_texture.texture) data_size = image.width * image.height * 4 data = bytes(rl.ffi.buffer(image.data, data_size)) - self._ffmpeg_proc.stdin.write(data) - self._ffmpeg_proc.stdin.flush() + self._ffmpeg_queue.put(data) # Async write via background thread rl.unload_image(image) self._monitor_fps() diff --git a/system/ui/lib/multilang.py b/system/ui/lib/multilang.py index 70de1e3d5c..ad2a5f9399 100644 --- a/system/ui/lib/multilang.py +++ b/system/ui/lib/multilang.py @@ -16,7 +16,6 @@ TRANSLATIONS_DIR = UI_DIR.joinpath("translations") LANGUAGES_FILE = TRANSLATIONS_DIR.joinpath("languages.json") UNIFONT_LANGUAGES = [ - "ar", "th", "zh-CHT", "zh-CHS", diff --git a/system/ui/lib/scroll_panel2.py b/system/ui/lib/scroll_panel2.py index 0859071dac..e2a548ba26 100644 --- a/system/ui/lib/scroll_panel2.py +++ b/system/ui/lib/scroll_panel2.py @@ -73,8 +73,14 @@ class GuiScrollPanel2: def _update_state(self, bounds_size: float, content_size: float) -> None: """Runs per render frame, independent of mouse events. Updates auto-scrolling state and velocity.""" - if self._state == ScrollState.AUTO_SCROLL: - max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size) + max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size) + + if self._state == ScrollState.STEADY: + # if we find ourselves out of bounds, scroll back in (from external layout dimension changes, etc.) + if self.get_offset() > max_offset or self.get_offset() < min_offset: + self._state = ScrollState.AUTO_SCROLL + + elif self._state == ScrollState.AUTO_SCROLL: # simple exponential return if out of bounds out_of_bounds = self.get_offset() > max_offset or self.get_offset() < min_offset if out_of_bounds and self._handle_out_of_bounds: diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index c98d9a6ae5..2c76862ce5 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -631,7 +631,7 @@ class WifiManager: known_connections = self._get_connections() networks = [Network.from_dbus(ssid, ap_list, ssid in known_connections) for ssid, ap_list in aps.items()] # sort with quantized strength to reduce jumping - networks.sort(key=lambda n: (-n.is_connected, -round(n.strength / 100 * 2), n.ssid.lower())) + networks.sort(key=lambda n: (-n.is_connected, -n.is_saved, -round(n.strength / 100 * 2), n.ssid.lower())) self._networks = networks self._update_ipv4_address() diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index adc3f125c8..3242cc0dbe 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -49,7 +49,7 @@ exec ./launch_openpilot.sh class NetworkConnectivityMonitor: - def __init__(self, should_check: Callable[[], bool] | None = None, check_interval: float = 0.5): + def __init__(self, should_check: Callable[[], bool] | None = None, check_interval: float = 1.0): self.network_connected = threading.Event() self.wifi_connected = threading.Event() self._should_check = should_check or (lambda: True) @@ -78,7 +78,7 @@ class NetworkConnectivityMonitor: if self._should_check(): try: request = urllib.request.Request(OPENPILOT_URL, method="HEAD") - urllib.request.urlopen(request, timeout=0.5) + urllib.request.urlopen(request, timeout=1.0) self.network_connected.set() if HARDWARE.get_network_type() == NetworkType.wifi: self.wifi_connected.set() @@ -94,12 +94,12 @@ class NetworkConnectivityMonitor: class SetupState(IntEnum): GETTING_STARTED = 0 NETWORK_SETUP = 1 - NETWORK_SETUP_CUSTOM_SOFTWARE = 8 - SOFTWARE_SELECTION = 2 - CUSTOM_SOFTWARE = 3 - DOWNLOADING = 4 - DOWNLOAD_FAILED = 5 - CUSTOM_SOFTWARE_WARNING = 6 + NETWORK_SETUP_CUSTOM_SOFTWARE = 2 + SOFTWARE_SELECTION = 3 + CUSTOM_SOFTWARE = 4 + DOWNLOADING = 5 + DOWNLOAD_FAILED = 6 + CUSTOM_SOFTWARE_WARNING = 7 class StartPage(Widget): @@ -528,9 +528,8 @@ class Setup(Widget): self.download_thread = None self._wifi_manager = WifiManager() self._wifi_manager.set_active(True) - self._network_monitor = NetworkConnectivityMonitor( - lambda: self.state in (SetupState.NETWORK_SETUP, SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE) - ) + self._network_monitor = NetworkConnectivityMonitor() + self._network_monitor.start() self._prev_has_internet = False gui_app.set_modal_overlay_tick(self._modal_overlay_tick) @@ -569,10 +568,8 @@ class Setup(Widget): if self.state in (SetupState.NETWORK_SETUP, SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE): self._network_setup_page.show_event() self._network_monitor.reset() - self._network_monitor.start() else: self._network_setup_page.hide_event() - self._network_monitor.stop() def _render(self, rect: rl.Rectangle): if self.state == SetupState.GETTING_STARTED: @@ -593,15 +590,9 @@ class Setup(Widget): def _custom_software_warning_back_button_callback(self): self._set_state(SetupState.SOFTWARE_SELECTION) - def _custom_software_warning_continue_button_callback(self): - self._set_state(SetupState.CUSTOM_SOFTWARE) - def _getting_started_button_callback(self): self._set_state(SetupState.SOFTWARE_SELECTION) - def _software_selection_back_button_callback(self): - self._set_state(SetupState.GETTING_STARTED) - def _software_selection_continue_button_callback(self): self.use_openpilot() @@ -618,7 +609,6 @@ class Setup(Widget): self._set_state(SetupState.SOFTWARE_SELECTION) def _network_setup_continue_button_callback(self): - self._network_monitor.stop() if self.state == SetupState.NETWORK_SETUP: self.download(OPENPILOT_URL) elif self.state == SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE: @@ -628,10 +618,10 @@ class Setup(Widget): self._network_monitor.stop() def render_network_setup(self, rect: rl.Rectangle): - self._network_setup_page.render(rect) has_internet = self._network_monitor.network_connected.is_set() self._prev_has_internet = has_internet self._network_setup_page.set_has_internet(has_internet) + self._network_setup_page.render(rect) def render_downloading(self, rect: rl.Rectangle): self._downloading_page.set_progress(self.download_progress) diff --git a/system/ui/sunnypilot/widgets/__init__.py b/system/ui/sunnypilot/widgets/__init__.py index e69de29bb2..96865e6042 100644 --- a/system/ui/sunnypilot/widgets/__init__.py +++ b/system/ui/sunnypilot/widgets/__init__.py @@ -0,0 +1,18 @@ +""" +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. +""" + + +def get_highlighted_description(params, param_name: str, descriptions: list[str]) -> str: + index = int(params.get(param_name, return_default=True)) + lines = [] + for i, desc in enumerate(descriptions): + if i == index: + lines.append(f"{desc}") + else: + lines.append(f"{desc}") + + return "
".join(lines) diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py index 39e88169d2..1dd69c24cb 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -134,6 +134,10 @@ class MultipleButtonActionSP(MultipleButtonAction): if self.param_key: self.selected_button = int(self.params.get(self.param_key, return_default=True)) self._anim_x: float | None = None + self.enabled_buttons: set[int] | None = None + + def set_enabled_buttons(self, indices: set[int] | None): + self.enabled_buttons = indices def _render(self, rect: rl.Rectangle): @@ -171,10 +175,31 @@ class MultipleButtonActionSP(MultipleButtonAction): text_x = button_x + (self.button_width - text_size.x) / 2 text_y = button_y + (style.BUTTON_HEIGHT - text_size.y) / 2 - rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, text_color) + # Check individual button enabled state + is_button_enabled = self.enabled and (self.enabled_buttons is None or i in self.enabled_buttons) + current_text_color = text_color if is_button_enabled else style.MBC_DISABLED + + rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, current_text_color) def _handle_mouse_release(self, mouse_pos: MousePos): - MultipleButtonAction._handle_mouse_release(self, mouse_pos) + # Override parent method to check individual button enabled state + if not self.enabled: + return + + button_y = self._rect.y + (self._rect.height - style.BUTTON_HEIGHT) / 2 + for i, _ in enumerate(self.buttons): + button_x = self._rect.x + i * self.button_width + button_rect = rl.Rectangle(button_x, button_y, self.button_width, style.BUTTON_HEIGHT) + + if rl.check_collision_point_rec(mouse_pos, button_rect): + # Check if this specific button is enabled + if self.enabled_buttons is not None and i not in self.enabled_buttons: + return + + self.selected_button = i + if self.callback: + self.callback(i) + if self.param_key: self.params.put(self.param_key, self.selected_button) @@ -212,6 +237,12 @@ class ListItemSP(ListItem): content_width = int(self._rect.width - style.ITEM_PADDING * 2) self._rect.height = self.get_item_height(self._font, content_width) + def set_parent_rect(self, parent_rect: rl.Rectangle) -> None: + super().set_parent_rect(parent_rect) + if self.description_visible: + content_width = int(self._rect.width - style.ITEM_PADDING * 2) + self._rect.height = self.get_item_height(self._font, content_width) + def get_item_height(self, font: rl.Font, max_width: int) -> float: height = super().get_item_height(font, max_width) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 42d9a910aa..b3b1276a58 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -195,7 +195,7 @@ NAV_BAR_WIDTH = 205 NAV_BAR_HEIGHT = 8 DISMISS_PUSH_OFFSET = 50 + NAV_BAR_MARGIN + NAV_BAR_HEIGHT # px extra to push down when dismissing -DISMISS_TIME_SECONDS = 1.5 +DISMISS_TIME_SECONDS = 2.0 class NavBar(Widget): @@ -242,6 +242,7 @@ class NavWidget(Widget, abc.ABC): self._pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) self._playing_dismiss_animation = False self._trigger_animate_in = False + self._nav_bar_show_time = 0.0 self._back_enabled: bool | Callable[[], bool] = True self._nav_bar = NavBar() @@ -330,6 +331,7 @@ class NavWidget(Widget, abc.ABC): if self._trigger_animate_in: self._pos_filter.x = self._rect.height self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT + self._nav_bar_show_time = rl.get_time() self._trigger_animate_in = False new_y = 0.0 @@ -366,18 +368,24 @@ class NavWidget(Widget, abc.ABC): if self.back_enabled: bar_x = self._rect.x + (self._rect.width - self._nav_bar.rect.width) / 2 + nav_bar_delayed = rl.get_time() - self._nav_bar_show_time < 0.4 + # User dragging or dismissing, nav bar follows NavWidget if self._back_button_start_pos is not None or self._playing_dismiss_animation: self._nav_bar_y_filter.x = NAV_BAR_MARGIN + self._pos_filter.x + # Waiting to show + elif nav_bar_delayed: + self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT + # Animate back to top else: self._nav_bar_y_filter.update(NAV_BAR_MARGIN) - self._nav_bar.set_position(bar_x, round(self._nav_bar_y_filter.x)) - self._nav_bar.render() - # draw black above widget when dismissing if self._rect.y > 0: rl.draw_rectangle(int(self._rect.x), 0, int(self._rect.width), int(self._rect.y), rl.BLACK) + self._nav_bar.set_position(bar_x, round(self._nav_bar_y_filter.x)) + self._nav_bar.render() + return ret def show_event(self): diff --git a/system/ui/widgets/mici_keyboard.py b/system/ui/widgets/mici_keyboard.py index 7459dc5731..6d2e08e053 100644 --- a/system/ui/widgets/mici_keyboard.py +++ b/system/ui/widgets/mici_keyboard.py @@ -38,10 +38,10 @@ def fast_euclidean_distance(dx, dy): class Key(Widget): - def __init__(self, char: str): + def __init__(self, char: str, font_weight: FontWeight = FontWeight.SEMI_BOLD): super().__init__() self.char = char - self._font = gui_app.font(FontWeight.SEMI_BOLD) + self._font = gui_app.font(font_weight) self._x_filter = BounceFilter(0.0, 0.1 * ANIMATION_SCALE, 1 / gui_app.target_fps) self._y_filter = BounceFilter(0.0, 0.1 * ANIMATION_SCALE, 1 / gui_app.target_fps) self._size_filter = BounceFilter(CHAR_FONT_SIZE, 0.1 * ANIMATION_SCALE, 1 / gui_app.target_fps) @@ -53,20 +53,23 @@ class Key(Widget): self.original_position = rl.Vector2(0, 0) def set_position(self, x: float, y: float, smooth: bool = True): - # TODO: swipe up from NavWidget has the keys lag behind other elements a bit + # Smooth keys within parent rect + base_y = self._parent_rect.y if self._parent_rect else 0.0 + local_y = y - base_y + if not self._position_initialized: self._x_filter.x = x - self._y_filter.x = y + self._y_filter.x = local_y # keep track of original position so dragging around feels consistent. also move touch area down a bit self.original_position = rl.Vector2(x, y + KEY_TOUCH_AREA_OFFSET) self._position_initialized = True if not smooth: self._x_filter.x = x - self._y_filter.x = y + self._y_filter.x = local_y self._rect.x = self._x_filter.update(x) - self._rect.y = self._y_filter.update(y) + self._rect.y = base_y + self._y_filter.update(local_y) def set_alpha(self, alpha: float): self._alpha_filter.update(alpha) @@ -97,7 +100,7 @@ class Key(Widget): class SmallKey(Key): def __init__(self, chars: str): - super().__init__(chars) + super().__init__(chars, FontWeight.BOLD) self._size_filter.x = NUMBER_LAYER_SWITCH_FONT_SIZE def set_font_size(self, size: float): @@ -105,13 +108,15 @@ class SmallKey(Key): class IconKey(Key): - def __init__(self, icon: str, vertical_align: str = "center", char: str = ""): + def __init__(self, icon: str, vertical_align: str = "center", char: str = "", icon_size: tuple[int, int] = (38, 38)): super().__init__(char) - self._icon = gui_app.texture(icon, 38, 38) + self._icon_size = icon_size + self._icon = gui_app.texture(icon, *icon_size) self._vertical_align = vertical_align - def set_icon(self, icon: str): - self._icon = gui_app.texture(icon, 38, 38) + def set_icon(self, icon: str, icon_size: tuple[int, int] | None = None): + size = icon_size if icon_size is not None else self._icon_size + self._icon = gui_app.texture(icon, *size) def _render(self, _): scale = np.interp(self._size_filter.x, [CHAR_FONT_SIZE, CHAR_NEAR_FONT_SIZE], [1, 1.5]) @@ -167,8 +172,8 @@ class MiciKeyboard(Widget): self._super_special_keys = [[Key(char) for char in row] for row in super_special_chars] # control keys - self._space_key = IconKey("icons_mici/settings/keyboard/space.png", char=" ", vertical_align="bottom") - self._caps_key = IconKey("icons_mici/settings/keyboard/caps_lower.png") + self._space_key = IconKey("icons_mici/settings/keyboard/space.png", char=" ", vertical_align="bottom", icon_size=(43, 14)) + self._caps_key = IconKey("icons_mici/settings/keyboard/caps_lower.png", icon_size=(38, 33)) # these two are in different places on some layouts self._123_key, self._123_key2 = SmallKey("123"), SmallKey("123") self._abc_key = SmallKey("abc") @@ -269,14 +274,14 @@ class MiciKeyboard(Widget): self._set_keys(self._upper_keys if cycle else self._lower_keys) if not cycle: self._caps_state = CapsState.LOWER - self._caps_key.set_icon("icons_mici/settings/keyboard/caps_lower.png") + self._caps_key.set_icon("icons_mici/settings/keyboard/caps_lower.png", icon_size=(38, 33)) else: if self._caps_state == CapsState.LOWER: self._caps_state = CapsState.UPPER - self._caps_key.set_icon("icons_mici/settings/keyboard/caps_upper.png") + self._caps_key.set_icon("icons_mici/settings/keyboard/caps_upper.png", icon_size=(38, 33)) elif self._caps_state == CapsState.UPPER: self._caps_state = CapsState.LOCK - self._caps_key.set_icon("icons_mici/settings/keyboard/caps_lock.png") + self._caps_key.set_icon("icons_mici/settings/keyboard/caps_lock.png", icon_size=(39, 38)) else: self._set_uppercase(False) @@ -365,6 +370,7 @@ class MiciKeyboard(Widget): key.set_font_size(font_size) # TODO: I like the push amount, so we should clip the pos inside the keyboard rect + key.set_parent_rect(self._rect) key.set_position(key_x, key_y) def _render(self, _): diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index f33ba941bf..03c49fca65 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -15,7 +15,6 @@ ANIMATION_SCALE = 0.6 MIN_ZOOM_ANIMATION_TIME = 0.075 # seconds DO_ZOOM = False DO_JELLO = False -SCROLL_BAR = False class LineSeparator(Widget): @@ -33,9 +32,52 @@ class LineSeparator(Widget): LINE_COLOR) +class ScrollIndicator(Widget): + HORIZONTAL_MARGIN = 4 + + def __init__(self): + super().__init__() + self._txt_scroll_indicator = gui_app.texture("icons_mici/settings/horizontal_scroll_indicator.png", 96, 48) + self._scroll_offset: float = 0.0 + self._content_size: float = 0.0 + self._viewport: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) + + def update(self, scroll_offset: float, content_size: float, viewport: rl.Rectangle) -> None: + self._scroll_offset = scroll_offset + self._content_size = content_size + self._viewport = viewport + + def _render(self, _): + # scale indicator width based on content size + indicator_w = float(np.interp(self._content_size, [1000, 3000], [300, 100])) + + # position based on scroll ratio + slide_range = self._viewport.width - indicator_w + max_scroll = self._content_size - self._viewport.width + scroll_ratio = -self._scroll_offset / max_scroll + x = self._viewport.x + scroll_ratio * slide_range + # don't bounce up when NavWidget shows + y = max(self._viewport.y, 0) + self._viewport.height - self._txt_scroll_indicator.height / 2 + + # squeeze when overscrolling past edges + dest_left = max(x, self._viewport.x) + dest_right = min(x + indicator_w, self._viewport.x + self._viewport.width) + dest_w = max(indicator_w / 2, dest_right - dest_left) + + # keep within viewport after applying minimum width + dest_left = min(dest_left, self._viewport.x + self._viewport.width - dest_w) + dest_left = max(dest_left, self._viewport.x) + + src_rec = rl.Rectangle(0, 0, self._txt_scroll_indicator.width, self._txt_scroll_indicator.height) + dest_rec = rl.Rectangle(dest_left, y, dest_w, self._txt_scroll_indicator.height) + rl.draw_texture_pro(self._txt_scroll_indicator, src_rec, dest_rec, rl.Vector2(0, 0), 0.0, + rl.Color(255, 255, 255, int(255 * 0.45))) + + class Scroller(Widget): def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = True, spacing: int = ITEM_SPACING, - line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING): + line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING, + scroll_indicator: bool = True): super().__init__() self._items: list[Widget] = [] self._horizontal = horizontal @@ -65,7 +107,8 @@ class Scroller(Widget): self.scroll_panel = GuiScrollPanel2(self._horizontal, handle_out_of_bounds=not self._snap_items) self._scroll_enabled: bool | Callable[[], bool] = True - self._txt_scroll_indicator = gui_app.texture("icons_mici/settings/vertical_scroll_indicator.png", 40, 80) + self._show_scroll_indicator = scroll_indicator + self._scroll_indicator = ScrollIndicator() for item in items: self.add_widget(item) @@ -241,15 +284,13 @@ class Scroller(Widget): else: item.render() - # Draw scroll indicator - if SCROLL_BAR and not self._horizontal and len(self._visible_items) > 0: - _real_content_size = self._content_size - self._rect.height + self._txt_scroll_indicator.height - scroll_bar_y = -self._scroll_offset / _real_content_size * self._rect.height - scroll_bar_y = min(max(scroll_bar_y, self._rect.y), self._rect.y + self._rect.height - self._txt_scroll_indicator.height) - rl.draw_texture_ex(self._txt_scroll_indicator, rl.Vector2(self._rect.x, scroll_bar_y), 0, 1.0, rl.WHITE) - rl.end_scissor_mode() + # Draw scroll indicator + if self._show_scroll_indicator and self._horizontal and len(self._visible_items) > 0: + self._scroll_indicator.update(self._scroll_offset, self._content_size, self._rect) + self._scroll_indicator.render() + def show_event(self): super().show_event() if self._reset_scroll_at_show: diff --git a/third_party/acados/.gitignore b/third_party/acados/.gitignore index 68858c62e4..0ae664dff6 100644 --- a/third_party/acados/.gitignore +++ b/third_party/acados/.gitignore @@ -1,5 +1,9 @@ acados_repo/ -lib +/lib !x86_64/ !larch64/ !aarch64/ +!Darwin/ +!*.so +!*.so.* +!*.dylib diff --git a/third_party/acados/acados_template/acados_ocp.py b/third_party/acados/acados_template/acados_ocp.py index ec02822ceb..d6236e1f6e 100644 --- a/third_party/acados/acados_template/acados_ocp.py +++ b/third_party/acados/acados_template/acados_ocp.py @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/acados_ocp_solver.py b/third_party/acados/acados_template/acados_ocp_solver.py index ffc9cf4b0e..229bdf6039 100644 --- a/third_party/acados/acados_template/acados_ocp_solver.py +++ b/third_party/acados/acados_template/acados_ocp_solver.py @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/acados_ocp_solver_pyx.pyx b/third_party/acados/acados_template/acados_ocp_solver_pyx.pyx index acd7f02d0a..bc03ba086f 100644 --- a/third_party/acados/acados_template/acados_ocp_solver_pyx.pyx +++ b/third_party/acados/acados_template/acados_ocp_solver_pyx.pyx @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/acados_sim.py b/third_party/acados/acados_template/acados_sim.py index c0d6937a49..7faa49fb12 100644 --- a/third_party/acados/acados_template/acados_sim.py +++ b/third_party/acados/acados_template/acados_sim.py @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/acados_sim_solver.py b/third_party/acados/acados_template/acados_sim_solver.py index 612f439eaf..de5ee10709 100644 --- a/third_party/acados/acados_template/acados_sim_solver.py +++ b/third_party/acados/acados_template/acados_sim_solver.py @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/acados_sim_solver_common.pxd b/third_party/acados/acados_template/acados_sim_solver_common.pxd index cc6a58efd7..7c20a6d24d 100644 --- a/third_party/acados/acados_template/acados_sim_solver_common.pxd +++ b/third_party/acados/acados_template/acados_sim_solver_common.pxd @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/acados_sim_solver_pyx.pyx b/third_party/acados/acados_template/acados_sim_solver_pyx.pyx index be400addc7..01964fd7a0 100644 --- a/third_party/acados/acados_template/acados_sim_solver_pyx.pyx +++ b/third_party/acados/acados_template/acados_sim_solver_pyx.pyx @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/acados_solver_common.pxd b/third_party/acados/acados_template/acados_solver_common.pxd index c6d59d40a5..75d021626f 100644 --- a/third_party/acados/acados_template/acados_solver_common.pxd +++ b/third_party/acados/acados_template/acados_solver_common.pxd @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/builders.py b/third_party/acados/acados_template/builders.py index 6f21bfe8cd..8acc05b528 100644 --- a/third_party/acados/acados_template/builders.py +++ b/third_party/acados/acados_template/builders.py @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright 2019 Gianluca Frison, Dimitris Kouzoupis, Robin Verschueren, # Andrea Zanelli, Niels van Duijkeren, Jonathan Frey, Tommaso Sartor, diff --git a/third_party/acados/acados_template/gnsf/__init__.py b/third_party/acados/acados_template/gnsf/__init__.py index e69de29bb2..8b13789179 100644 --- a/third_party/acados/acados_template/gnsf/__init__.py +++ b/third_party/acados/acados_template/gnsf/__init__.py @@ -0,0 +1 @@ + diff --git a/third_party/acados/acados_template/utils.py b/third_party/acados/acados_template/utils.py index d6f6c02f84..f27617fa30 100644 --- a/third_party/acados/acados_template/utils.py +++ b/third_party/acados/acados_template/utils.py @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/build.sh b/third_party/acados/build.sh index b45c167b16..2c4d839ae7 100755 --- a/third_party/acados/build.sh +++ b/third_party/acados/build.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash set -e +export SOURCE_DATE_EPOCH=0 +export ZERO_AR_DATE=1 + DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" ARCHNAME="x86_64" @@ -44,6 +47,12 @@ cp -r $DIR/acados_repo/lib $INSTALL_DIR cp -r $DIR/acados_repo/interfaces/acados_template/acados_template $DIR/ #pip3 install -e $DIR/acados/interfaces/acados_template +# skip macOS - sed is different :/ +if [[ "$OSTYPE" != "darwin"* ]]; then + # strip future_fstrings to avoid having to install the compatibility package + find $DIR/acados_template/ -type f -exec sed -i '/future.fstrings/d' {} + +fi + # build tera cd $DIR/acados_repo/interfaces/acados_template/tera_renderer/ if [[ "$OSTYPE" == "darwin"* ]]; then diff --git a/third_party/acados/x86_64/lib/libacados.so b/third_party/acados/x86_64/lib/libacados.so index 4e80f7c76b..50d647a862 100644 --- a/third_party/acados/x86_64/lib/libacados.so +++ b/third_party/acados/x86_64/lib/libacados.so @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:821ce18f417d211c4845b60482d465b809f90dc7d04f023d652d8221e87679b1 -size 553544 +oid sha256:05a1ba3cf37fa929cdd56f892608b2f89c35a05ef1b07fedb86b2f0d76607263 +size 540488 diff --git a/third_party/acados/x86_64/lib/libblasfeo.so b/third_party/acados/x86_64/lib/libblasfeo.so index 26d5a3dbe9..a98f45abd2 100644 --- a/third_party/acados/x86_64/lib/libblasfeo.so +++ b/third_party/acados/x86_64/lib/libblasfeo.so @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3feea7927d004064bbc5a13c3287467669ce801cb0a3c616cf9e089816da5a0b -size 2155088 +oid sha256:c0bf22898d9c59b672d3d0961f5f4c804b9957478125d99eb297de3091bedd15 +size 2416112 diff --git a/third_party/acados/x86_64/lib/libhpipm.so b/third_party/acados/x86_64/lib/libhpipm.so index 40e2e4e7d4..f40cb487cd 100644 --- a/third_party/acados/x86_64/lib/libhpipm.so +++ b/third_party/acados/x86_64/lib/libhpipm.so @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a042716f515913786581dff39799eb71fc66caddfa18b1c9f0d54f00c1568fd2 -size 1572648 +oid sha256:5b6875fb47940764d4ebb916c2373cb0e04929229feb654b290676c28d48fa9d +size 1531024 diff --git a/third_party/acados/x86_64/lib/libqpOASES_e.so.3.1 b/third_party/acados/x86_64/lib/libqpOASES_e.so.3.1 index cf5e550faa..81afd059f7 100644 --- a/third_party/acados/x86_64/lib/libqpOASES_e.so.3.1 +++ b/third_party/acados/x86_64/lib/libqpOASES_e.so.3.1 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6abea4815e3f03cff06fe8a9602e97f9acf102f18f803571460a94595b93be4 -size 262824 +oid sha256:04be908c3f707e5c968022b9cdd79ab75ae7af46e7fa019ceee98f854ddd3f64 +size 262464 diff --git a/third_party/acados/x86_64/t_renderer b/third_party/acados/x86_64/t_renderer index e995a209b7..d41f6c3725 100755 --- a/third_party/acados/x86_64/t_renderer +++ b/third_party/acados/x86_64/t_renderer @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a360d4b53826b91ada3358156d44a14d497bdd8ace88707fd4b386ed6d194c7 -size 17503920 +oid sha256:a53ae46650c4df5b0ddb87a658f59a0422e41743e8bc2d822da0aefd1d280791 +size 5088536 diff --git a/third_party/build.sh b/third_party/build.sh new file mode 100755 index 0000000000..d3a9c6579c --- /dev/null +++ b/third_party/build.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" + +# Reproducible builds: pin timestamps to epoch +export SOURCE_DATE_EPOCH=0 +export ZERO_AR_DATE=1 + +pids=() +names=() +logs=() + +for script in "$DIR"/*/build.sh; do + [ -f "$script" ] || continue + name=$(basename "$(dirname "$script")") + log=$(mktemp) + names+=("$name") + logs+=("$log") + (cd "$(dirname "$script")" && bash "$(basename "$script")") >"$log" 2>&1 & + pids+=($!) +done + +failed=0 +for i in "${!pids[@]}"; do + echo "--- ${names[$i]} ---" + if wait "${pids[$i]}"; then + echo "OK" + else + echo "FAILED (exit $?)" + failed=1 + fi + cat "${logs[$i]}" + rm -f "${logs[$i]}" + echo +done + +[ $failed -ne 0 ] && exit $failed + +# Repack ar archives with deterministic headers (zero timestamps/uid/gid) +# Skip foreign-platform archives that ar can't read (e.g. Mach-O on Linux) +while IFS= read -r -d '' lib; do + tmpdir=$(mktemp -d) + lib=$(realpath "$lib") + if (cd "$tmpdir" && ar x "$lib" 2>/dev/null); then + (cd "$tmpdir" && ar Drcs repacked.a * && mv repacked.a "$lib") + fi + rm -rf "$tmpdir" +done < <(find "$DIR" -name '*.a' \ + \( -path '*/x86_64/*' -o -path '*/Darwin/*' -o -path '*/larch64/*' -o -path '*/aarch64/*' \) \ + -print0) + +echo -e "\033[32mAll third_party builds succeeded.\033[0m" diff --git a/third_party/libyuv/.gitignore b/third_party/libyuv/.gitignore index 450712e47d..1e943ae6c6 100644 --- a/third_party/libyuv/.gitignore +++ b/third_party/libyuv/.gitignore @@ -1 +1,2 @@ -libyuv/ +/libyuv/ +!*.a diff --git a/third_party/libyuv/build.sh b/third_party/libyuv/build.sh index b960f60ef5..35a7d947a2 100755 --- a/third_party/libyuv/build.sh +++ b/third_party/libyuv/build.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash set -e +export SOURCE_DATE_EPOCH=0 +export ZERO_AR_DATE=1 + DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" ARCHNAME=$(uname -m) diff --git a/third_party/libyuv/larch64/lib/libyuv.a b/third_party/libyuv/larch64/lib/libyuv.a index 1c91250231..9c4a32bcdb 100644 --- a/third_party/libyuv/larch64/lib/libyuv.a +++ b/third_party/libyuv/larch64/lib/libyuv.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:320bef5a75a62dd2731a496040921d5000f1ed237ae70fd7aeb6c010a1534363 +oid sha256:adafce26582e425164df7af36253ce58e3ed1dba9965650745c93bd96e42e976 size 462482 diff --git a/third_party/libyuv/x86_64/include b/third_party/libyuv/x86_64/include deleted file mode 120000 index f5030fe889..0000000000 --- a/third_party/libyuv/x86_64/include +++ /dev/null @@ -1 +0,0 @@ -../include \ No newline at end of file diff --git a/third_party/libyuv/x86_64/lib/libyuv.a b/third_party/libyuv/x86_64/lib/libyuv.a index 8915f167dc..391b1c8769 100644 --- a/third_party/libyuv/x86_64/lib/libyuv.a +++ b/third_party/libyuv/x86_64/lib/libyuv.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e21a3bd8df01cf4ce5461e7bf6654239196036c3f829255145265c7bf31a791d -size 511974 +oid sha256:00f9759c67c6fa21657fabde9e096478ea5809716989599f673f638f039431e5 +size 504790 diff --git a/third_party/raylib/.gitignore b/third_party/raylib/.gitignore index c4afad9c38..6b1d3ad748 100644 --- a/third_party/raylib/.gitignore +++ b/third_party/raylib/.gitignore @@ -1,3 +1,4 @@ /raylib_repo/ /raylib_python_repo/ /wheel/ +!*.a diff --git a/third_party/raylib/Darwin/libraylib.a b/third_party/raylib/Darwin/libraylib.a index 837ad8818e..dd2e9b33f1 100644 --- a/third_party/raylib/Darwin/libraylib.a +++ b/third_party/raylib/Darwin/libraylib.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ffe1fc6497f0c111fc507988e94fd29ce4db53a4876dc82ab9267895ad82584 -size 6515352 +oid sha256:fd045c1d4bca5c9b2ad044ea730826ff6cedeef0b64451b123717b136f1cd702 +size 6392532 diff --git a/third_party/raylib/build.sh b/third_party/raylib/build.sh index 7f2ce5951f..d20f9d33af 100755 --- a/third_party/raylib/build.sh +++ b/third_party/raylib/build.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash set -e +export SOURCE_DATE_EPOCH=0 +export ZERO_AR_DATE=1 + SUDO="" # Use sudo if not root diff --git a/third_party/raylib/larch64/libraylib.a b/third_party/raylib/larch64/libraylib.a index 4e810c8b7b..fa538e5214 100644 --- a/third_party/raylib/larch64/libraylib.a +++ b/third_party/raylib/larch64/libraylib.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91e9a07513e84f7b553da01b34b24e12fe7130131ef73ebdb3dac3b838db815b +oid sha256:f760af8b4693cf60e3760341e5275890d78d933da2354c4bad0572ec575b970a size 2001860 diff --git a/third_party/raylib/x86_64/libraylib.a b/third_party/raylib/x86_64/libraylib.a index cf69482563..ea124c1bcf 100644 --- a/third_party/raylib/x86_64/libraylib.a +++ b/third_party/raylib/x86_64/libraylib.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0b8f59758fe1291be82a8bda7a7ca05629c7addb0683936dd404ed08e19e143 -size 2769684 +oid sha256:3c928e849b51b04d8e3603cd649184299efed0e9e0fb02201612b967b31efd73 +size 2771092 diff --git a/tinygrad_repo b/tinygrad_repo index 7296c74cbd..3501a71478 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 7296c74cbd2666da7dce95d7ca6dab5340653a5c +Subproject commit 3501a714785ff370cffb966a45d5f9cdf6c9ea7a diff --git a/tools/clip/run.py b/tools/clip/run.py index 9045a4381b..d338f097d6 100755 --- a/tools/clip/run.py +++ b/tools/clip/run.py @@ -1,310 +1,358 @@ #!/usr/bin/env python3 - -import logging import os -import platform -import shutil import sys import time -from argparse import ArgumentParser, ArgumentTypeError -from collections.abc import Sequence +import logging +import subprocess +import threading +import queue +import multiprocessing +import itertools +import numpy as np +import tqdm +from argparse import ArgumentParser from pathlib import Path -from random import randint -from subprocess import Popen -from typing import Literal +from concurrent.futures import ThreadPoolExecutor, as_completed -from cereal.messaging import SubMaster -from openpilot.common.basedir import BASEDIR -from openpilot.common.params import Params, UnknownKeyName -from openpilot.common.prefix import OpenpilotPrefix -from openpilot.common.utils import managed_proc from openpilot.tools.lib.route import Route from openpilot.tools.lib.logreader import LogReader +from openpilot.tools.lib.filereader import FileReader +from openpilot.tools.lib.framereader import FrameReader, ffprobe +from openpilot.selfdrive.test.process_replay.migration import migrate_all +from openpilot.common.prefix import OpenpilotPrefix +from openpilot.common.utils import Timer +from msgq.visionipc import VisionIpcServer, VisionStreamType -DEFAULT_OUTPUT = 'output.mp4' -DEMO_START = 90 -DEMO_END = 105 -DEMO_ROUTE = 'a2a0ccea32023010/2023-07-27--13-01-19' FRAMERATE = 20 -PIXEL_DEPTH = '24' -RESOLUTION = '2160x1080' -SECONDS_TO_WARM = 2 -PROC_WAIT_SECONDS = 30*10 +DEMO_ROUTE, DEMO_START, DEMO_END = 'a2a0ccea32023010/2023-07-27--13-01-19', 90, 105 -OPENPILOT_FONT = str(Path(BASEDIR, 'selfdrive/assets/fonts/Inter-Regular.ttf').resolve()) -REPLAY = str(Path(BASEDIR, 'tools/replay/replay').resolve()) -UI = str(Path(BASEDIR, 'selfdrive/ui/ui').resolve()) - -logger = logging.getLogger('clip.py') +logger = logging.getLogger('clip') -def check_for_failure(procs: list[Popen]): - for proc in procs: - exit_code = proc.poll() - if exit_code is not None and exit_code != 0: - cmd = str(proc.args) - if isinstance(proc.args, str): - cmd = proc.args - elif isinstance(proc.args, Sequence): - cmd = str(proc.args[0]) - msg = f'{cmd} failed, exit code {exit_code}' - logger.error(msg) - stdout, stderr = proc.communicate() - if stdout: - logger.error(stdout.decode()) - if stderr: - logger.error(stderr.decode()) - raise ChildProcessError(msg) - - -def escape_ffmpeg_text(value: str): - special_chars = {',': '\\,', ':': '\\:', '=': '\\=', '[': '\\[', ']': '\\]'} - value = value.replace('\\', '\\\\\\\\\\\\\\\\') - for char, escaped in special_chars.items(): - value = value.replace(char, escaped) - return value - - -def get_logreader(route: Route): - return LogReader(route.qlog_paths()[0] if len(route.qlog_paths()) else route.name.canonical_name) - - -def get_meta_text(lr: LogReader, route: Route): - init_data = lr.first('initData') - car_params = lr.first('carParams') - origin_parts = init_data.gitRemote.split('/') - origin = origin_parts[3] if len(origin_parts) > 3 else 'unknown' - return ', '.join([ - f"openpilot v{init_data.version}", - f"route: {route.name.canonical_name}", - f"car: {car_params.carFingerprint}", - f"origin: {origin}", - f"branch: {init_data.gitBranch}", - f"commit: {init_data.gitCommit[:7]}", - f"modified: {str(init_data.dirty).lower()}", - ]) - - -def parse_args(parser: ArgumentParser): +def parse_args(): + parser = ArgumentParser(description="Direct clip renderer") + parser.add_argument("route", nargs="?", help="Route ID (dongle/route or dongle/route/start/end)") + parser.add_argument("-s", "--start", type=int, help="Start time in seconds") + parser.add_argument("-e", "--end", type=int, help="End time in seconds") + parser.add_argument("-o", "--output", default="output.mp4", help="Output file path") + parser.add_argument("-d", "--data-dir", help="Local directory with route data") + parser.add_argument("-t", "--title", help="Title overlay text") + parser.add_argument("-f", "--file-size", type=float, default=9.0, help="Target file size in MB") + parser.add_argument("-x", "--speed", type=int, default=1, help="Speed multiplier") + parser.add_argument("--demo", action="store_true", help="Use demo route with default timing") + parser.add_argument("--big", action="store_true", help="Use big UI (2160x1080)") + parser.add_argument("--qcam", action="store_true", help="Use qcamera instead of fcamera") + parser.add_argument("--windowed", action="store_true", help="Show window") + parser.add_argument("--no-metadata", action="store_true", help="Disable metadata overlay") + parser.add_argument("--no-time-overlay", action="store_true", help="Disable time overlay") args = parser.parse_args() + if args.demo: - args.route = DEMO_ROUTE - if args.start is None or args.end is None: - args.start = DEMO_START - args.end = DEMO_END - elif args.route.count('/') == 1: - if args.start is None or args.end is None: - parser.error('must provide both start and end if timing is not in the route ID') - elif args.route.count('/') == 3: - if args.start is not None or args.end is not None: - parser.error('don\'t provide timing when including it in the route ID') + args.route, args.start, args.end = args.route or DEMO_ROUTE, args.start or DEMO_START, args.end or DEMO_END + elif not args.route: + parser.error("route is required (or use --demo)") + + if args.route and args.route.count('/') == 3: parts = args.route.split('/') - args.route = '/'.join(parts[:2]) - args.start = int(parts[2]) - args.end = int(parts[3]) + args.route, args.start, args.end = '/'.join(parts[:2]), args.start or int(parts[2]), args.end or int(parts[3]) + + if args.start is None or args.end is None: + parser.error("--start and --end are required") if args.end <= args.start: - parser.error(f'end ({args.end}) must be greater than start ({args.start})') - if args.start < SECONDS_TO_WARM: - parser.error(f'start must be greater than {SECONDS_TO_WARM}s to allow the UI time to warm up') - - try: - args.route = Route(args.route, data_dir=args.data_dir) - except Exception as e: - parser.error(f'failed to get route: {e}') - - # FIXME: length isn't exactly max segment seconds, simplify to replay exiting at end of data - length = round(args.route.max_seg_number * 60) - if args.start >= length: - parser.error(f'start ({args.start}s) cannot be after end of route ({length}s)') - if args.end > length: - parser.error(f'end ({args.end}s) cannot be after end of route ({length}s)') - + parser.error(f"end ({args.end}) must be greater than start ({args.start})") return args -def populate_car_params(lr: LogReader): - init_data = lr.first('initData') - assert init_data is not None +def setup_env(output_path: str, big: bool = False, speed: int = 1, target_mb: float = 0, duration: int = 0): + os.environ.update({"RECORD": "1", "OFFSCREEN": "1", "RECORD_OUTPUT": str(Path(output_path).with_suffix(".mp4"))}) + if speed > 1: + os.environ["RECORD_SPEED"] = str(speed) + if target_mb > 0 and duration > 0: + os.environ["RECORD_BITRATE"] = f"{int(target_mb * 8 * 1024 / (duration / speed))}k" + if big: + os.environ["BIG"] = "1" + + +def _download_segment(path: str) -> bytes: + with FileReader(path) as f: + return bytes(f.read()) + + +def _parse_and_chunk_segment(args: tuple) -> list[dict]: + raw_data, fps = args + from openpilot.tools.lib.logreader import _LogFileReader + messages = migrate_all(list(_LogFileReader("", dat=raw_data, sort_by_time=True))) + if not messages: + return [] + + dt_ns, chunks, current, next_time = 1e9 / fps, [], {}, messages[0].logMonoTime + 1e9 / fps # type: ignore[var-annotated] + for msg in messages: + if msg.logMonoTime >= next_time: + chunks.append(current) + current, next_time = {}, next_time + dt_ns * ((msg.logMonoTime - next_time) // dt_ns + 1) + current[msg.which()] = msg + return chunks + [current] if current else chunks + + +def load_logs_parallel(log_paths: list[str], fps: int = 20) -> list[dict]: + num_workers = min(16, len(log_paths), (multiprocessing.cpu_count() or 1)) + logger.info(f"Downloading {len(log_paths)} segments with {num_workers} workers...") + + with ThreadPoolExecutor(max_workers=num_workers) as pool: + futures = {pool.submit(_download_segment, path): idx for idx, path in enumerate(log_paths)} + raw_data = {futures[f]: f.result() for f in as_completed(futures)} + + logger.info("Parsing and chunking segments...") + with multiprocessing.Pool(num_workers) as pool: + return list(itertools.chain.from_iterable(pool.map(_parse_and_chunk_segment, [(raw_data[i], fps) for i in range(len(log_paths))]))) + + +def patch_submaster(message_chunks, ui_state): + # Reset started_frame so alerts render correctly (recv_frame must be >= started_frame) + ui_state.started_frame = 0 + ui_state.started_time = time.monotonic() + + def mock_update(timeout=None): + sm, t = ui_state.sm, time.monotonic() + sm.updated = dict.fromkeys(sm.services, False) + if sm.frame < len(message_chunks): + for svc, msg in message_chunks[sm.frame].items(): + if svc in sm.data: + sm.seen[svc] = sm.updated[svc] = sm.alive[svc] = sm.valid[svc] = True + sm.data[svc] = getattr(msg.as_builder(), svc) + sm.logMonoTime[svc], sm.recv_time[svc], sm.recv_frame[svc] = msg.logMonoTime, t, sm.frame + sm.frame += 1 + ui_state.sm.update = mock_update + + +def get_frame_dimensions(camera_path: str) -> tuple[int, int]: + """Get frame dimensions from a video file using ffprobe.""" + probe = ffprobe(camera_path) + stream = probe["streams"][0] + return stream["width"], stream["height"] + + +def iter_segment_frames(camera_paths, start_time, end_time, fps=20, use_qcam=False, frame_size: tuple[int, int] | None = None): + frames_per_seg = fps * 60 + start_frame, end_frame = int(start_time * fps), int(end_time * fps) + current_seg: int = -1 + seg_frames: FrameReader | np.ndarray | None = None + + for global_idx in range(start_frame, end_frame): + seg_idx, local_idx = global_idx // frames_per_seg, global_idx % frames_per_seg + + if seg_idx != current_seg: + current_seg = seg_idx + path = camera_paths[seg_idx] if seg_idx < len(camera_paths) else None + if not path: + raise RuntimeError(f"No camera file for segment {seg_idx}") + + if use_qcam: + w, h = frame_size or get_frame_dimensions(path) + with FileReader(path) as f: + result = subprocess.run(["ffmpeg", "-v", "quiet", "-i", "-", "-f", "rawvideo", "-pix_fmt", "nv12", "-"], + input=f.read(), capture_output=True) + if result.returncode != 0: + raise RuntimeError(f"ffmpeg failed: {result.stderr.decode()}") + seg_frames = np.frombuffer(result.stdout, dtype=np.uint8).reshape(-1, w * h * 3 // 2) + else: + seg_frames = FrameReader(path, pix_fmt="nv12") + + assert seg_frames is not None + frame = seg_frames[local_idx] if use_qcam else seg_frames.get(local_idx) # type: ignore[index, union-attr] + yield global_idx, frame + + +class FrameQueue: + def __init__(self, camera_paths, start_time, end_time, fps=20, prefetch_count=60, use_qcam=False): + # Probe first valid camera file for dimensions + first_path = next((p for p in camera_paths if p), None) + if not first_path: + raise RuntimeError("No valid camera paths") + self.frame_w, self.frame_h = get_frame_dimensions(first_path) + + self._queue, self._stop, self._error = queue.Queue(maxsize=prefetch_count), threading.Event(), None + self._thread = threading.Thread(target=self._worker, + args=(camera_paths, start_time, end_time, fps, use_qcam, (self.frame_w, self.frame_h)), daemon=True) + self._thread.start() + + def _worker(self, camera_paths, start_time, end_time, fps, use_qcam, frame_size): + try: + for idx, data in iter_segment_frames(camera_paths, start_time, end_time, fps, use_qcam, frame_size): + if self._stop.is_set(): + break + self._queue.put((idx, data.tobytes())) + except Exception as e: + logger.exception("Decode error") + self._error = e + finally: + self._queue.put(None) + + def get(self, timeout=60.0): + if self._error: + raise self._error + result = self._queue.get(timeout=timeout) + if result is None: + raise StopIteration("No more frames") + return result + + def stop(self): + self._stop.set() + while not self._queue.empty(): + try: + self._queue.get_nowait() + except queue.Empty: + break + self._thread.join(timeout=2.0) + + +def load_route_metadata(route): + from openpilot.common.params import Params, UnknownKeyName + path = next((item for item in route.log_paths() if item), None) + if not path: + raise Exception('error getting route metadata: cannot find any uploaded logs') + lr = LogReader(path) + init_data, car_params = lr.first('initData'), lr.first('carParams') params = Params() - entries = init_data.params.entries - for cp in entries: - key, value = cp.key, cp.value + for entry in init_data.params.entries: try: - params.put(key, params.cpp2python(key, value)) + params.put(entry.key, params.cpp2python(entry.key, entry.value)) except UnknownKeyName: - # forks of openpilot may have other Params keys configured. ignore these - logger.warning(f"unknown Params key '{key}', skipping") - logger.debug('persisted CarParams') + pass + + origin = init_data.gitRemote.split('/')[3] if len(init_data.gitRemote.split('/')) > 3 else 'unknown' + return { + 'version': init_data.version, 'route': route.name.canonical_name, + 'car': car_params.carFingerprint if car_params else 'unknown', 'origin': origin, + 'branch': init_data.gitBranch, 'commit': init_data.gitCommit[:7], 'modified': str(init_data.dirty).lower(), + } -def validate_env(parser: ArgumentParser): - if platform.system() not in ['Linux']: - parser.exit(1, f'clip.py: error: {platform.system()} is not a supported operating system\n') - for proc in ['Xvfb', 'ffmpeg']: - if shutil.which(proc) is None: - parser.exit(1, f'clip.py: error: missing {proc} command, is it installed?\n') - for proc in [REPLAY, UI]: - if shutil.which(proc) is None: - parser.exit(1, f'clip.py: error: missing {proc} command, did you build openpilot yet?\n') +def draw_text_box(text, x, y, size, gui_app, font, color=None, center=False): + import pyray as rl + from openpilot.system.ui.lib.text_measure import measure_text_cached + box_color, text_color = rl.Color(0, 0, 0, 85), color or rl.WHITE + text_size = measure_text_cached(font, text, size) + text_width, text_height = int(text_size.x), int(text_size.y) + if center: + x = (gui_app.width - text_width) // 2 + rl.draw_rectangle(x - 8, y - 4, text_width + 16, text_height + 8, box_color) + rl.draw_text_ex(font, text, rl.Vector2(x, y), size, 0, text_color) -def validate_output_file(output_file: str): - if not output_file.endswith('.mp4'): - raise ArgumentTypeError('output must be an mp4') - return output_file +def render_overlays(gui_app, font, big, metadata, title, start_time, frame_idx, show_metadata, show_time): + from openpilot.system.ui.lib.text_measure import measure_text_cached + from openpilot.system.ui.lib.wrap_text import wrap_text + metadata_size = 16 if big else 12 + title_size = 32 if big else 24 + time_size = 24 if big else 16 + # Time overlay + time_width = 0 + if show_time: + t = start_time + frame_idx / FRAMERATE + time_text = f"{int(t) // 60:02d}:{int(t) % 60:02d}" + time_width = int(measure_text_cached(font, time_text, time_size).x) + draw_text_box(time_text, gui_app.width - time_width - 5, 0, time_size, gui_app, font) -def validate_route(route: str): - if route.count('/') not in (1, 3): - raise ArgumentTypeError(f'route must include or exclude timing, example: {DEMO_ROUTE}') - return route + # Metadata overlay (first 5 seconds) + if show_metadata and metadata and frame_idx < FRAMERATE * 5: + m = metadata + text = ", ".join([f"openpilot v{m['version']}", f"route: {m['route']}", f"car: {m['car']}", f"origin: {m['origin']}", + f"branch: {m['branch']}", f"commit: {m['commit']}", f"modified: {m['modified']}"]) + # Wrap text if too wide (leave margin on each side) + margin = 2 * (time_width + 10 if show_time else 20) # leave enough margin for time overlay + max_width = gui_app.width - margin + lines = wrap_text(font, text, metadata_size, max_width) + # Draw wrapped metadata text + y_offset = 6 + for line in lines: + draw_text_box(line, 0, y_offset, metadata_size, gui_app, font, center=True) + line_height = int(measure_text_cached(font, line, metadata_size).y) + 4 + y_offset += line_height -def validate_title(title: str): - if len(title) > 80: - raise ArgumentTypeError('title must be no longer than 80 chars') - return title - - -def wait_for_frames(procs: list[Popen]): - sm = SubMaster(['uiDebug']) - no_frames_drawn = True - while no_frames_drawn: - sm.update() - no_frames_drawn = sm['uiDebug'].drawTimeMillis == 0. - check_for_failure(procs) - - -def clip( - data_dir: str | None, - quality: Literal['low', 'high'], - prefix: str, - route: Route, - out: str, - start: int, - end: int, - speed: int, - target_mb: int, - title: str | None, -): - logger.info(f'clipping route {route.name.canonical_name}, start={start} end={end} quality={quality} target_filesize={target_mb}MB') - lr = get_logreader(route) - - begin_at = max(start - SECONDS_TO_WARM, 0) - duration = end - start - bit_rate_kbps = int(round(target_mb * 8 * 1024 * 1024 / duration / 1000)) - - # TODO: evaluate creating fn that inspects /tmp/.X11-unix and creates unused display to avoid possibility of collision - display = f':{randint(99, 999)}' - - box_style = 'box=1:boxcolor=black@0.33:boxborderw=7' - meta_text = get_meta_text(lr, route) - overlays = [ - # metadata overlay - f"drawtext=text='{escape_ffmpeg_text(meta_text)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=15:{box_style}:x=(w-text_w)/2:y=5.5:enable='between(t,1,5)'", - # route time overlay - f"drawtext=text='%{{eif\\:floor(({start}+t)/60)\\:d\\:2}}\\:%{{eif\\:mod({start}+t\\,60)\\:d\\:2}}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=24:{box_style}:x=w-text_w-38:y=38" - ] + # Title overlay if title: - overlays.append(f"drawtext=text='{escape_ffmpeg_text(title)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=32:{box_style}:x=(w-text_w)/2:y=53") + draw_text_box(title, 0, 60, title_size, gui_app, font, center=True) - if speed > 1: - overlays += [ - f"setpts=PTS/{speed}", - "fps=60", - ] - ffmpeg_cmd = [ - 'ffmpeg', '-y', - '-video_size', RESOLUTION, - '-framerate', str(FRAMERATE), - '-f', 'x11grab', - '-rtbufsize', '100M', - '-draw_mouse', '0', - '-i', display, - '-c:v', 'libx264', - '-maxrate', f'{bit_rate_kbps}k', - '-bufsize', f'{bit_rate_kbps*2}k', - '-crf', '23', - '-filter:v', ','.join(overlays), - '-preset', 'ultrafast', - '-tune', 'zerolatency', - '-pix_fmt', 'yuv420p', - '-movflags', '+faststart', - '-f', 'mp4', - '-t', str(duration), - out, - ] +def clip(route: Route, output: str, start: int, end: int, headless: bool = True, big: bool = False, + title: str | None = None, show_metadata: bool = True, show_time: bool = True, use_qcam: bool = False): + timer, duration = Timer(), end - start - replay_cmd = [REPLAY, '--ecam', '-c', '1', '-s', str(begin_at), '--prefix', prefix] - if data_dir: - replay_cmd.extend(['--data_dir', data_dir]) - if quality == 'low': - replay_cmd.append('--qcam') - replay_cmd.append(route.name.canonical_name) + import pyray as rl + if big: + from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView + else: + from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView # type: ignore[assignment] + from openpilot.selfdrive.ui.ui_state import ui_state + from openpilot.system.ui.lib.application import gui_app, FontWeight + timer.lap("import") - ui_cmd = [UI, '-platform', 'xcb'] - xvfb_cmd = ['Xvfb', display, '-terminate', '-screen', '0', f'{RESOLUTION}x{PIXEL_DEPTH}'] + logger.info(f"Clipping {route.name.canonical_name}, {start}s-{end}s ({duration}s)") + seg_start, seg_end = start // 60, (end - 1) // 60 + 1 + all_chunks = load_logs_parallel(route.log_paths()[seg_start:seg_end], fps=FRAMERATE) + timer.lap("logs") - with OpenpilotPrefix(prefix, shared_download_cache=True): - populate_car_params(lr) - env = os.environ.copy() - env['DISPLAY'] = display + frame_start = (start - seg_start * 60) * FRAMERATE + message_chunks = all_chunks[frame_start:frame_start + duration * FRAMERATE] + if not message_chunks: + logger.error("No messages to render") + sys.exit(1) - with managed_proc(xvfb_cmd, env) as xvfb_proc, managed_proc(ui_cmd, env) as ui_proc, managed_proc(replay_cmd, env) as replay_proc: - procs = [xvfb_proc, ui_proc, replay_proc] - logger.info('waiting for replay to begin (loading segments, may take a while)...') - wait_for_frames(procs) - logger.debug(f'letting UI warm up ({SECONDS_TO_WARM}s)...') - time.sleep(SECONDS_TO_WARM) - check_for_failure(procs) - with managed_proc(ffmpeg_cmd, env) as ffmpeg_proc: - procs.append(ffmpeg_proc) - logger.info(f'recording in progress ({duration}s)...') - ffmpeg_proc.wait(duration + PROC_WAIT_SECONDS) - check_for_failure(procs) - logger.info(f'recording complete: {Path(out).resolve()}') + metadata = load_route_metadata(route) if show_metadata else None + if headless: + rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN) + + with OpenpilotPrefix(shared_download_cache=True): + camera_paths = route.qcamera_paths() if use_qcam else route.camera_paths() + frame_queue = FrameQueue(camera_paths, start, end, fps=FRAMERATE, use_qcam=use_qcam) + + vipc = VisionIpcServer("camerad") + vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 4, frame_queue.frame_w, frame_queue.frame_h) + vipc.start_listener() + + patch_submaster(message_chunks, ui_state) + gui_app.init_window("clip", fps=FRAMERATE) + + road_view = AugmentedRoadView() + road_view.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + font = gui_app.font(FontWeight.NORMAL) + timer.lap("setup") + + frame_idx = 0 + with tqdm.tqdm(total=len(message_chunks), desc="Rendering", unit="frame") as pbar: + for should_render in gui_app.render(): + if frame_idx >= len(message_chunks): + break + _, frame_bytes = frame_queue.get() + vipc.send(VisionStreamType.VISION_STREAM_ROAD, frame_bytes, frame_idx, int(frame_idx * 5e7), int(frame_idx * 5e7)) + ui_state.update() + if should_render: + road_view.render() + render_overlays(gui_app, font, big, metadata, title, start, frame_idx, show_metadata, show_time) + frame_idx += 1 + pbar.update(1) + timer.lap("render") + + frame_queue.stop() + gui_app.close() + timer.lap("ffmpeg") + + logger.info(f"Clip saved to: {Path(output).resolve()}") + logger.info(f"Generated {timer.fmt(duration)}") def main(): - p = ArgumentParser(prog='clip.py', description='clip your openpilot route.', epilog='comma.ai') - validate_env(p) - route_group = p.add_mutually_exclusive_group(required=True) - route_group.add_argument('route', nargs='?', type=validate_route, help=f'The route (e.g. {DEMO_ROUTE} or {DEMO_ROUTE}/{DEMO_START}/{DEMO_END})') - route_group.add_argument('--demo', help='use the demo route', action='store_true') - p.add_argument('-d', '--data-dir', help='local directory where route data is stored') - p.add_argument('-e', '--end', help='stop clipping at seconds', type=int) - p.add_argument('-f', '--file-size', help='target file size (Discord/GitHub support max 10MB, default is 9MB)', type=float, default=9.) - p.add_argument('-o', '--output', help='output clip to (.mp4)', type=validate_output_file, default=DEFAULT_OUTPUT) - p.add_argument('-p', '--prefix', help='openpilot prefix', default=f'clip_{randint(100, 99999)}') - p.add_argument('-q', '--quality', help='quality of camera (low = qcam, high = hevc)', choices=['low', 'high'], default='high') - p.add_argument('-x', '--speed', help='record the clip at this speed multiple', type=int, default=1) - p.add_argument('-s', '--start', help='start clipping at seconds', type=int) - p.add_argument('-t', '--title', help='overlay this title on the video (e.g. "Chill driving across the Golden Gate Bridge")', type=validate_title) - args = parse_args(p) - exit_code = 1 - try: - clip( - data_dir=args.data_dir, - quality=args.quality, - prefix=args.prefix, - route=args.route, - out=args.output, - start=args.start, - end=args.end, - speed=args.speed, - target_mb=args.file_size, - title=args.title, - ) - exit_code = 0 - except KeyboardInterrupt as e: - logger.exception('interrupted by user', exc_info=e) - except Exception as e: - logger.exception('encountered error', exc_info=e) - sys.exit(exit_code) + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s\t%(message)s") + args = parse_args() + + setup_env(args.output, big=args.big, speed=args.speed, target_mb=args.file_size, duration=args.end - args.start) + clip(Route(args.route, data_dir=args.data_dir), args.output, args.start, args.end, not args.windowed, + args.big, args.title, not args.no_metadata, not args.no_time_overlay, args.qcam) -if __name__ == '__main__': - logging.basicConfig(level=logging.INFO, format='%(asctime)s %(name)s %(levelname)s\t%(message)s') +if __name__ == "__main__": main() diff --git a/tools/lib/openpilotci.py b/tools/lib/openpilotci.py index 1c1e1f171b..6b484dfdad 100644 --- a/tools/lib/openpilotci.py +++ b/tools/lib/openpilotci.py @@ -1,12 +1,4 @@ -from openpilot.tools.lib.openpilotcontainers import OpenpilotCIContainer +BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/" -def get_url(*args, **kwargs): - return OpenpilotCIContainer.get_url(*args, **kwargs) - -def upload_file(*args, **kwargs): - return OpenpilotCIContainer.upload_file(*args, **kwargs) - -def upload_bytes(*args, **kwargs): - return OpenpilotCIContainer.upload_bytes(*args, **kwargs) - -BASE_URL = OpenpilotCIContainer.BASE_URL +def get_url(route_name: str, segment_num, filename: str) -> str: + return BASE_URL + f"{route_name.replace('|', '/')}/{segment_num}/{filename}" diff --git a/tools/lib/tests/test_caching.py b/tools/lib/tests/test_caching.py index 6e70ef90b0..cb14098e6d 100644 --- a/tools/lib/tests/test_caching.py +++ b/tools/lib/tests/test_caching.py @@ -56,13 +56,13 @@ class TestFileDownload: for k, v in retry_defaults.items(): assert getattr(URLFile.pool_manager().connection_pool_kw["retries"], k) == v - # ensure caching off by default and cache dir doesn't get created - os.environ.pop("FILEREADER_CACHE", None) + # ensure caching on by default and cache dir gets created + os.environ.pop("DISABLE_FILEREADER_CACHE", None) if os.path.exists(Paths.download_cache_root()): shutil.rmtree(Paths.download_cache_root()) URLFile(f"{host}/test.txt").get_length() URLFile(f"{host}/test.txt").read() - assert not os.path.exists(Paths.download_cache_root()) + assert os.path.exists(Paths.download_cache_root()) def compare_loads(self, url, start=0, length=None): """Compares range between cached and non cached version""" @@ -90,7 +90,7 @@ class TestFileDownload: def test_small_file(self): # Make sure we don't force cache - os.environ["FILEREADER_CACHE"] = "0" + os.environ.pop("DISABLE_FILEREADER_CACHE", None) small_file_url = "https://raw.githubusercontent.com/commaai/openpilot/master/docs/SAFETY.md" # If you want large file to be larger than a chunk # large_file_url = "https://commadataci.blob.core.windows.net/openpilotci/0375fdf7b1ce594d/2019-06-13--08-32-25/3/fcamera.hevc" @@ -119,7 +119,10 @@ class TestFileDownload: @pytest.mark.parametrize("cache_enabled", [True, False]) def test_recover_from_missing_file(self, host, cache_enabled): - os.environ["FILEREADER_CACHE"] = "1" if cache_enabled else "0" + if cache_enabled: + os.environ.pop("DISABLE_FILEREADER_CACHE", None) + else: + os.environ["DISABLE_FILEREADER_CACHE"] = "1" file_url = f"{host}/test.png" diff --git a/tools/lib/tests/test_logreader.py b/tools/lib/tests/test_logreader.py index ee75a8b1ce..0151940c44 100644 --- a/tools/lib/tests/test_logreader.py +++ b/tools/lib/tests/test_logreader.py @@ -93,7 +93,10 @@ class TestLogReader: @pytest.mark.parametrize("cache_enabled", [True, False]) def test_direct_parsing(self, mocker, cache_enabled): file_exists_mock = mocker.patch("openpilot.tools.lib.filereader.file_exists") - os.environ["FILEREADER_CACHE"] = "1" if cache_enabled else "0" + if cache_enabled: + os.environ.pop("DISABLE_FILEREADER_CACHE", None) + else: + os.environ["DISABLE_FILEREADER_CACHE"] = "1" qlog = tempfile.NamedTemporaryFile(mode='wb', delete=False) with requests.get(QLOG_FILE, stream=True) as r: @@ -181,7 +184,10 @@ class TestLogReader: @parameterized.expand([(True,), (False,)]) @pytest.mark.slow def test_run_across_segments(self, cache_enabled): - os.environ["FILEREADER_CACHE"] = "1" if cache_enabled else "0" + if cache_enabled: + os.environ.pop("DISABLE_FILEREADER_CACHE", None) + else: + os.environ["DISABLE_FILEREADER_CACHE"] = "1" lr = LogReader(f"{TEST_ROUTE}/0:4") assert len(lr.run_across_segments(4, noop)) == len(list(lr)) diff --git a/tools/lib/url_file.py b/tools/lib/url_file.py index 790fa7e8fb..de12070465 100644 --- a/tools/lib/url_file.py +++ b/tools/lib/url_file.py @@ -74,8 +74,8 @@ class URLFile: self._timeout = Timeout(connect=timeout, read=timeout) self._pos = 0 self._length: int | None = None - # True by default, false if FILEREADER_CACHE is defined, but can be overwritten by the cache input - self._force_download = not int(os.environ.get("FILEREADER_CACHE", "0")) + # Caching enabled by default, can be disabled with DISABLE_FILEREADER_CACHE=1, or overwritten by the cache input + self._force_download = int(os.environ.get("DISABLE_FILEREADER_CACHE", "0")) == 1 if cache is not None: self._force_download = not cache @@ -192,8 +192,25 @@ class URLFile: raise URLFileException(f"Expected {len(ranges)} parts, got {len(parts)} ({self._url})") return parts - def seek(self, pos: int) -> None: - self._pos = int(pos) + def seekable(self) -> bool: + return True + + def seek(self, pos: int, whence: int = 0) -> int: + pos = int(pos) + if whence == os.SEEK_SET: + self._pos = pos + elif whence == os.SEEK_CUR: + self._pos += pos + elif whence == os.SEEK_END: + length = self.get_length() + assert length != -1, "Cannot seek from end on unknown length file" + self._pos = length + pos + else: + raise URLFileException("Invalid whence value") + return self._pos + + def tell(self) -> int: + return self._pos @property def name(self) -> str: diff --git a/tools/longitudinal_maneuvers/generate_report.py b/tools/longitudinal_maneuvers/generate_report.py index 8c16e30d56..32bdb5b1c4 100755 --- a/tools/longitudinal_maneuvers/generate_report.py +++ b/tools/longitudinal_maneuvers/generate_report.py @@ -9,7 +9,7 @@ import webbrowser from collections import defaultdict from pathlib import Path import matplotlib.pyplot as plt -from tabulate import tabulate +from openpilot.common.utils import tabulate from openpilot.tools.lib.logreader import LogReader from openpilot.system.hardware.hw import Paths diff --git a/tools/longitudinal_maneuvers/maneuversd.py b/tools/longitudinal_maneuvers/maneuversd.py index c17ae23757..9044afaa8f 100755 --- a/tools/longitudinal_maneuvers/maneuversd.py +++ b/tools/longitudinal_maneuvers/maneuversd.py @@ -132,7 +132,7 @@ def main(): CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) sm = messaging.SubMaster(['carState', 'carControl', 'controlsState', 'selfdriveState', 'modelV2'], poll='modelV2') - pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'alertDebug']) + pm = messaging.PubMaster(['longitudinalPlan', 'longitudinalPlanSP', 'driverAssistance', 'alertDebug']) maneuvers = iter(MANEUVERS) maneuver = None @@ -177,6 +177,10 @@ def main(): pm.send('longitudinalPlan', plan_send) + plan_sp_send = messaging.new_message('longitudinalPlanSP') + plan_sp_send.valid = True + pm.send('longitudinalPlanSP', plan_sp_send) + assistance_send = messaging.new_message('driverAssistance') assistance_send.valid = True pm.send('driverAssistance', assistance_send) diff --git a/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py b/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py new file mode 100644 index 0000000000..7623f669cd --- /dev/null +++ b/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py @@ -0,0 +1,309 @@ +import io +import sys +import markdown +import numpy as np +import matplotlib.pyplot as plt +from openpilot.common.realtime import DT_MDL +from openpilot.selfdrive.controls.tests.test_following_distance import desired_follow_distance +from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver + +TIME = 0 +LEAD_DISTANCE= 2 +EGO_V = 3 +EGO_A = 5 +D_REL = 6 + +axis_labels = ['Time (s)', + 'Ego position (m)', + 'Lead absolute position (m)', + 'Ego Velocity (m/s)', + 'Lead Velocity (m/s)', + 'Ego acceleration (m/s^2)', + 'Lead distance (m)' + ] + +def get_html_from_results(results, labels, AXIS): + fig, ax = plt.subplots(figsize=(16, 8)) + for idx, speed in enumerate(list(results.keys())): + ax.plot(results[speed][:, TIME], results[speed][:, AXIS], label=labels[idx]) + + ax.set_xlabel('Time (s)') + ax.set_ylabel(axis_labels[AXIS]) + ax.legend(bbox_to_anchor=(1.02, 1), loc='upper left', borderaxespad=0) + ax.grid(True, linestyle='--', alpha=0.7) + ax.text(-0.075, 0.5, '.', transform=ax.transAxes, color='none') + + fig_buffer = io.StringIO() + fig.savefig(fig_buffer, format='svg', bbox_inches='tight') + plt.close(fig) + return fig_buffer.getvalue() + '
' + +def generate_mpc_tuning_report(): + htmls = [] + + results = {} + name = 'Resuming behind lead' + labels = [] + for lead_accel in np.linspace(1.0, 4.0, 4): + man = Maneuver( + '', + duration=11, + initial_speed=0.0, + lead_relevancy=True, + initial_distance_lead=desired_follow_distance(0.0, 0.0), + speed_lead_values=[0.0, 10 * lead_accel], + cruise_values=[100, 100], + prob_lead_values=[1.0, 1.0], + breakpoints=[1., 11], + ) + valid, results[lead_accel] = man.evaluate() + labels.append(f'{lead_accel} m/s^2 lead acceleration') + + htmls.append(markdown.markdown('# ' + name)) + htmls.append(get_html_from_results(results, labels, EGO_V)) + htmls.append(get_html_from_results(results, labels, EGO_A)) + + + results = {} + name = 'Approaching stopped car from 140m' + labels = [] + for speed in np.arange(0,45,5): + man = Maneuver( + name, + duration=30., + initial_speed=float(speed), + lead_relevancy=True, + initial_distance_lead=140., + speed_lead_values=[0.0, 0.], + breakpoints=[0., 30.], + ) + valid, results[speed] = man.evaluate() + results[speed][:,2] = results[speed][:,2] - results[speed][:,1] + labels.append(f'{speed} m/s approach speed') + + htmls.append(markdown.markdown('# ' + name)) + htmls.append(get_html_from_results(results, labels, EGO_A)) + htmls.append(get_html_from_results(results, labels, D_REL)) + + + results = {} + name = 'Following 5s (triangular) oscillating lead' + labels = [] + speed = np.int64(10) + for oscil in np.arange(0, 10, 1): + man = Maneuver( + '', + duration=30., + initial_speed=float(speed), + lead_relevancy=True, + initial_distance_lead=desired_follow_distance(speed, speed), + speed_lead_values=[speed, speed, speed - oscil, speed + oscil, speed - oscil, speed + oscil, speed - oscil], + breakpoints=[0.,2., 5, 8, 15, 18, 25.], + ) + valid, results[oscil] = man.evaluate() + labels.append(f'{oscil} m/s oscillation size') + + htmls.append(markdown.markdown('# ' + name)) + htmls.append(get_html_from_results(results, labels, D_REL)) + htmls.append(get_html_from_results(results, labels, EGO_V)) + htmls.append(get_html_from_results(results, labels, EGO_A)) + + + results = {} + name = 'Following 5s (sinusoidal) oscillating lead' + labels = [] + speed = np.int64(10) + duration = float(30) + f_osc = 1. / 5 + for oscil in np.arange(0, 10, 1): + bps = DT_MDL * np.arange(int(duration / DT_MDL)) + lead_speeds = speed + oscil * np.sin(2 * np.pi * f_osc * bps) + man = Maneuver( + '', + duration=duration, + initial_speed=float(speed), + lead_relevancy=True, + initial_distance_lead=desired_follow_distance(speed, speed), + speed_lead_values=lead_speeds, + breakpoints=bps, + ) + valid, results[oscil] = man.evaluate() + labels.append(f'{oscil} m/s oscilliation size') + + htmls.append(markdown.markdown('# ' + name)) + htmls.append(get_html_from_results(results, labels, D_REL)) + htmls.append(get_html_from_results(results, labels, EGO_V)) + htmls.append(get_html_from_results(results, labels, EGO_A)) + + + results = {} + name = 'Speed profile when converging to steady state lead at 30m/s' + labels = [] + for distance in np.arange(20, 140, 10): + man = Maneuver( + '', + duration=50, + initial_speed=30.0, + lead_relevancy=True, + initial_distance_lead=distance, + speed_lead_values=[30.0], + breakpoints=[0.], + ) + valid, results[distance] = man.evaluate() + results[distance][:,2] = results[distance][:,2] - results[distance][:,1] + labels.append(f'{distance} m initial distance') + + htmls.append(markdown.markdown('# ' + name)) + htmls.append(get_html_from_results(results, labels, EGO_V)) + htmls.append(get_html_from_results(results, labels, D_REL)) + + + results = {} + name = 'Speed profile when converging to steady state lead at 20m/s' + labels = [] + for distance in np.arange(20, 140, 10): + man = Maneuver( + '', + duration=50, + initial_speed=20.0, + lead_relevancy=True, + initial_distance_lead=distance, + speed_lead_values=[20.0], + breakpoints=[0.], + ) + valid, results[distance] = man.evaluate() + results[distance][:,2] = results[distance][:,2] - results[distance][:,1] + labels.append(f'{distance} m initial distance') + + htmls.append(markdown.markdown('# ' + name)) + htmls.append(get_html_from_results(results, labels, EGO_V)) + htmls.append(get_html_from_results(results, labels, D_REL)) + + + results = {} + name = 'Following car at 30m/s that comes to a stop' + labels = [] + for stop_time in np.arange(4, 14, 1): + man = Maneuver( + '', + duration=30, + initial_speed=30.0, + cruise_values=[30.0, 30.0, 30.0], + lead_relevancy=True, + initial_distance_lead=60.0, + speed_lead_values=[30.0, 30.0, 0.0], + breakpoints=[0., 5., 5 + stop_time], + ) + valid, results[stop_time] = man.evaluate() + results[stop_time][:,2] = results[stop_time][:,2] - results[stop_time][:,1] + labels.append(f'{stop_time} seconds stop time') + + htmls.append(markdown.markdown('# ' + name)) + htmls.append(get_html_from_results(results, labels, EGO_A)) + htmls.append(get_html_from_results(results, labels, D_REL)) + + + results = {} + name = 'Response to cut-in at half follow distance' + labels = [] + for speed in np.arange(0, 40, 5): + man = Maneuver( + '', + duration=20, + initial_speed=float(speed), + cruise_values=[speed, speed, speed], + lead_relevancy=True, + initial_distance_lead=desired_follow_distance(speed, speed)/2, + speed_lead_values=[speed, speed, speed], + prob_lead_values=[0.0, 0.0, 1.0], + breakpoints=[0., 5.0, 5.01], + ) + valid, results[speed] = man.evaluate() + labels.append(f'{speed} m/s speed') + + htmls.append(markdown.markdown('# ' + name)) + htmls.append(get_html_from_results(results, labels, EGO_A)) + htmls.append(get_html_from_results(results, labels, D_REL)) + + + results = {} + name = 'Follow a lead that accelerates at 2m/s^2 until steady state speed' + labels = [] + for speed in np.arange(0, 40, 5): + man = Maneuver( + '', + duration=60, + initial_speed=0.0, + lead_relevancy=True, + initial_distance_lead=desired_follow_distance(0.0, 0.0), + speed_lead_values=[0.0, 0.0, speed], + prob_lead_values=[1.0, 1.0, 1.0], + breakpoints=[0., 1.0, speed/2], + ) + valid, results[speed] = man.evaluate() + labels.append(f'{speed} m/s speed') + + htmls.append(markdown.markdown('# ' + name)) + htmls.append(get_html_from_results(results, labels, EGO_V)) + htmls.append(get_html_from_results(results, labels, EGO_A)) + + + results = {} + name = 'From stop to cruise' + labels = [] + for speed in np.arange(0, 40, 5): + man = Maneuver( + '', + duration=50, + initial_speed=0.0, + lead_relevancy=True, + initial_distance_lead=desired_follow_distance(0.0, 0.0), + speed_lead_values=[0.0, 0.0], + cruise_values=[0.0, speed], + prob_lead_values=[0.0, 0.0], + breakpoints=[1., 1.01], + ) + valid, results[speed] = man.evaluate() + labels.append(f'{speed} m/s speed') + + htmls.append(markdown.markdown('# ' + name)) + htmls.append(get_html_from_results(results, labels, EGO_V)) + htmls.append(get_html_from_results(results, labels, EGO_A)) + + + results = {} + name = 'From cruise to min' + labels = [] + for speed in np.arange(10, 40, 5): + man = Maneuver( + '', + duration=50, + initial_speed=float(speed), + lead_relevancy=True, + initial_distance_lead=desired_follow_distance(0.0, 0.0), + speed_lead_values=[0.0, 0.0], + cruise_values=[speed, 10.0], + prob_lead_values=[0.0, 0.0], + breakpoints=[1., 1.01], + ) + valid, results[speed] = man.evaluate() + labels.append(f'{speed} m/s speed') + + htmls.append(markdown.markdown('# ' + name)) + htmls.append(get_html_from_results(results, labels, EGO_V)) + htmls.append(get_html_from_results(results, labels, EGO_A)) + + return htmls + +if __name__ == '__main__': + htmls = generate_mpc_tuning_report() + + if len(sys.argv) < 2: + file_name = 'long_mpc_tune_report.html' + else: + file_name = sys.argv[1] + + with open(file_name, 'w') as f: + f.write(markdown.markdown('# MPC longitudinal tuning report')) + for html in htmls: + f.write(html) diff --git a/tools/op.sh b/tools/op.sh index 8b5062ad9b..8c41926e0c 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -161,9 +161,9 @@ function op_check_python() { loge "ERROR_PYTHON_NOT_FOUND" return 1 else - LB=$(echo $REQUIRED_PYTHON_VERSION | tr -d -c '[0-9,]' | cut -d ',' -f1) - UB=$(echo $REQUIRED_PYTHON_VERSION | tr -d -c '[0-9,]' | cut -d ',' -f2) - VERSION=$(echo $INSTALLED_PYTHON_VERSION | grep -o '[0-9]\+\.[0-9]\+' | tr -d -c '[0-9]') + LB=$(echo $REQUIRED_PYTHON_VERSION | tr -d '",' | awk '{ split($4, v, "."); printf "%d%02d%02d", v[1], v[2], v[3] }') + UB=$(echo $REQUIRED_PYTHON_VERSION | tr -d '",' | awk '{ split($6, v, "."); printf "%d%02d%02d", v[1], v[2], v[3] }') + VERSION=$(echo $INSTALLED_PYTHON_VERSION | awk '{ split($2, v, "."); printf "%d%02d%02d", v[1], v[2], v[3] }') if [[ $VERSION -ge LB && $VERSION -lt UB ]]; then echo -e " ↳ [${GREEN}✔${NC}] $INSTALLED_PYTHON_VERSION detected." else diff --git a/tools/plotjuggler/juggle.py b/tools/plotjuggler/juggle.py index 34f33d1959..142e640504 100755 --- a/tools/plotjuggler/juggle.py +++ b/tools/plotjuggler/juggle.py @@ -47,7 +47,7 @@ def install(): tmpf.write(chunk) with tarfile.open(tmp.name) as tar: - tar.extractall(path=INSTALL_DIR) + tar.extractall(path=INSTALL_DIR, filter="data") def get_plotjuggler_version(): diff --git a/tools/replay/can_replay.py b/tools/replay/can_replay.py index 13c30a62ad..0717320077 100755 --- a/tools/replay/can_replay.py +++ b/tools/replay/can_replay.py @@ -5,8 +5,6 @@ import time import usb1 import threading -os.environ['FILEREADER_CACHE'] = '1' - from openpilot.common.realtime import config_realtime_process, Ratekeeper, DT_CTRL from openpilot.selfdrive.pandad import can_capnp_to_list from openpilot.tools.lib.logreader import LogReader diff --git a/tools/replay/ui.py b/tools/replay/ui.py index 54667ebfe9..8707f2be99 100755 --- a/tools/replay/ui.py +++ b/tools/replay/ui.py @@ -160,9 +160,12 @@ def ui_thread(addr): camera = DEVICE_CAMERAS[("tici", str(sm['roadCameraState'].sensor))] - imgff = np.frombuffer(yuv_img_raw.data, dtype=np.uint8).reshape((len(yuv_img_raw.data) // vipc_client.stride, vipc_client.stride)) - num_px = vipc_client.width * vipc_client.height - rgb = cv2.cvtColor(imgff[: vipc_client.height * 3 // 2, : vipc_client.width], cv2.COLOR_YUV2RGB_NV12) + # Use received buffer dimensions (full HEVC can have stride != buffer_len/rows due to VENUS padding) + h, w, stride = yuv_img_raw.height, yuv_img_raw.width, yuv_img_raw.stride + nv12_size = h * 3 // 2 * stride + imgff = np.frombuffer(yuv_img_raw.data, dtype=np.uint8, count=nv12_size).reshape((h * 3 // 2, stride)) + num_px = w * h + rgb = cv2.cvtColor(imgff[: h * 3 // 2, : w], cv2.COLOR_YUV2RGB_NV12) qcam = "QCAM" in os.environ bb_scale = (528 if qcam else camera.fcam.width) / 640.0 diff --git a/tools/sim/lib/camerad.py b/tools/sim/lib/camerad.py index be4e1a610c..7634b8524d 100644 --- a/tools/sim/lib/camerad.py +++ b/tools/sim/lib/camerad.py @@ -1,14 +1,39 @@ import numpy as np -import os -import pyopencl as cl -import pyopencl.array as cl_array from msgq.visionipc import VisionIpcServer, VisionStreamType from cereal import messaging -from openpilot.common.basedir import BASEDIR from openpilot.tools.sim.lib.common import W, H + +def rgb_to_nv12(rgb): + """Convert RGB image to NV12 (YUV420) format using BT.601 coefficients.""" + h, w = rgb.shape[:2] + r = rgb[:, :, 0].astype(np.int32) + g = rgb[:, :, 1].astype(np.int32) + b = rgb[:, :, 2].astype(np.int32) + + # Y plane - BT.601 coefficients (matches original OpenCL kernel) + y = (((b * 13 + g * 65 + r * 33) + 64) >> 7) + 16 + y = np.clip(y, 0, 255).astype(np.uint8) + + # Subsample RGB for UV (2x2 box filter) + r_sub = (r[0::2, 0::2] + r[0::2, 1::2] + r[1::2, 0::2] + r[1::2, 1::2] + 2) >> 2 + g_sub = (g[0::2, 0::2] + g[0::2, 1::2] + g[1::2, 0::2] + g[1::2, 1::2] + 2) >> 2 + b_sub = (b[0::2, 0::2] + b[0::2, 1::2] + b[1::2, 0::2] + b[1::2, 1::2] + 2) >> 2 + + # U and V planes + u = np.clip((b_sub * 56 - g_sub * 37 - r_sub * 19 + 0x8080) >> 8, 0, 255).astype(np.uint8) + v = np.clip((r_sub * 56 - g_sub * 47 - b_sub * 9 + 0x8080) >> 8, 0, 255).astype(np.uint8) + + # Interleave UV for NV12 format + uv = np.empty((h // 2, w), dtype=np.uint8) + uv[:, 0::2] = u + uv[:, 1::2] = v + + return np.concatenate([y.ravel(), uv.ravel()]).tobytes() + + class Camerad: """Simulates the camerad daemon""" def __init__(self, dual_camera): @@ -24,18 +49,6 @@ class Camerad: self.vipc_server.start_listener() - # set up for pyopencl rgb to yuv conversion - self.ctx = cl.create_some_context() - self.queue = cl.CommandQueue(self.ctx) - cl_arg = f" -DHEIGHT={H} -DWIDTH={W} -DRGB_STRIDE={W * 3} -DUV_WIDTH={W // 2} -DUV_HEIGHT={H // 2} -DRGB_SIZE={W * H} -DCL_DEBUG " - - kernel_fn = os.path.join(BASEDIR, "tools/sim/rgb_to_nv12.cl") - with open(kernel_fn) as f: - prg = cl.Program(self.ctx, f.read()).build(cl_arg) - self.krnl = prg.rgb_to_nv12 - self.Wdiv4 = W // 4 if (W % 4 == 0) else (W + (4 - W % 4)) // 4 - self.Hdiv4 = H // 4 if (H % 4 == 0) else (H + (4 - H % 4)) // 4 - def cam_send_yuv_road(self, yuv): self._send_yuv(yuv, self.frame_road_id, 'roadCameraState', VisionStreamType.VISION_STREAM_ROAD) self.frame_road_id += 1 @@ -44,16 +57,11 @@ class Camerad: self._send_yuv(yuv, self.frame_wide_id, 'wideRoadCameraState', VisionStreamType.VISION_STREAM_WIDE_ROAD) self.frame_wide_id += 1 - # Returns: yuv bytes def rgb_to_yuv(self, rgb): + """Convert RGB to NV12 YUV format.""" assert rgb.shape == (H, W, 3), f"{rgb.shape}" assert rgb.dtype == np.uint8 - - rgb_cl = cl_array.to_device(self.queue, rgb) - yuv_cl = cl_array.empty_like(rgb_cl) - self.krnl(self.queue, (self.Wdiv4, self.Hdiv4), None, rgb_cl.data, yuv_cl.data).wait() - yuv = np.resize(yuv_cl.get(), rgb.size // 2) - return yuv.data.tobytes() + return rgb_to_nv12(rgb) def _send_yuv(self, yuv, frame_id, pub_type, yuv_type): eof = int(frame_id * 0.05 * 1e9) diff --git a/tools/sim/rgb_to_nv12.cl b/tools/sim/rgb_to_nv12.cl deleted file mode 100644 index 54816d5d7d..0000000000 --- a/tools/sim/rgb_to_nv12.cl +++ /dev/null @@ -1,119 +0,0 @@ -#define RGB_TO_Y(r, g, b) ((((mul24(b, 13) + mul24(g, 65) + mul24(r, 33)) + 64) >> 7) + 16) -#define RGB_TO_U(r, g, b) ((mul24(b, 56) - mul24(g, 37) - mul24(r, 19) + 0x8080) >> 8) -#define RGB_TO_V(r, g, b) ((mul24(r, 56) - mul24(g, 47) - mul24(b, 9) + 0x8080) >> 8) -#define AVERAGE(x, y, z, w) ((convert_ushort(x) + convert_ushort(y) + convert_ushort(z) + convert_ushort(w) + 1) >> 1) - -inline void convert_2_ys(__global uchar * out_yuv, int yi, const uchar8 rgbs1) { - uchar2 yy = (uchar2)( - RGB_TO_Y(rgbs1.s2, rgbs1.s1, rgbs1.s0), - RGB_TO_Y(rgbs1.s5, rgbs1.s4, rgbs1.s3) - ); -#ifdef CL_DEBUG - if(yi >= RGB_SIZE) - printf("Y vector2 overflow, %d > %d\n", yi, RGB_SIZE); -#endif - vstore2(yy, 0, out_yuv + yi); -} - -inline void convert_4_ys(__global uchar * out_yuv, int yi, const uchar8 rgbs1, const uchar8 rgbs3) { - const uchar4 yy = (uchar4)( - RGB_TO_Y(rgbs1.s2, rgbs1.s1, rgbs1.s0), - RGB_TO_Y(rgbs1.s5, rgbs1.s4, rgbs1.s3), - RGB_TO_Y(rgbs3.s0, rgbs1.s7, rgbs1.s6), - RGB_TO_Y(rgbs3.s3, rgbs3.s2, rgbs3.s1) - ); -#ifdef CL_DEBUG - if(yi > RGB_SIZE - 4) - printf("Y vector4 overflow, %d > %d\n", yi, RGB_SIZE - 4); -#endif - vstore4(yy, 0, out_yuv + yi); -} - -inline void convert_uv(__global uchar * out_yuv, int uvi, - const uchar8 rgbs1, const uchar8 rgbs2) { - // U & V: average of 2x2 pixels square - const short ab = AVERAGE(rgbs1.s0, rgbs1.s3, rgbs2.s0, rgbs2.s3); - const short ag = AVERAGE(rgbs1.s1, rgbs1.s4, rgbs2.s1, rgbs2.s4); - const short ar = AVERAGE(rgbs1.s2, rgbs1.s5, rgbs2.s2, rgbs2.s5); -#ifdef CL_DEBUG - if(uvi >= RGB_SIZE + RGB_SIZE / 2) - printf("UV overflow, %d >= %d\n", uvi, RGB_SIZE + RGB_SIZE / 2); -#endif - out_yuv[uvi] = RGB_TO_U(ar, ag, ab); - out_yuv[uvi+1] = RGB_TO_V(ar, ag, ab); -} - -inline void convert_2_uvs(__global uchar * out_yuv, int uvi, - const uchar8 rgbs1, const uchar8 rgbs2, const uchar8 rgbs3, const uchar8 rgbs4) { - // U & V: average of 2x2 pixels square - const short ab1 = AVERAGE(rgbs1.s0, rgbs1.s3, rgbs2.s0, rgbs2.s3); - const short ag1 = AVERAGE(rgbs1.s1, rgbs1.s4, rgbs2.s1, rgbs2.s4); - const short ar1 = AVERAGE(rgbs1.s2, rgbs1.s5, rgbs2.s2, rgbs2.s5); - const short ab2 = AVERAGE(rgbs1.s6, rgbs3.s1, rgbs2.s6, rgbs4.s1); - const short ag2 = AVERAGE(rgbs1.s7, rgbs3.s2, rgbs2.s7, rgbs4.s2); - const short ar2 = AVERAGE(rgbs3.s0, rgbs3.s3, rgbs4.s0, rgbs4.s3); - uchar4 uv = (uchar4)( - RGB_TO_U(ar1, ag1, ab1), - RGB_TO_V(ar1, ag1, ab1), - RGB_TO_U(ar2, ag2, ab2), - RGB_TO_V(ar2, ag2, ab2) - ); -#ifdef CL_DEBUG1 - if(uvi > RGB_SIZE + RGB_SIZE / 2 - 4) - printf("UV2 overflow, %d >= %d\n", uvi, RGB_SIZE + RGB_SIZE / 2 - 2); -#endif - vstore4(uv, 0, out_yuv + uvi); -} - -__kernel void rgb_to_nv12(__global uchar const * const rgb, - __global uchar * out_yuv) -{ - const int dx = get_global_id(0); - const int dy = get_global_id(1); - const int col = mul24(dx, 4); // Current column in rgb image - const int row = mul24(dy, 4); // Current row in rgb image - const int bgri_start = mad24(row, RGB_STRIDE, mul24(col, 3)); // Start offset of rgb data being converted - const int yi_start = mad24(row, WIDTH, col); // Start offset in the target yuv buffer - int uvi = mad24(row / 2, WIDTH, RGB_SIZE + col); - int num_col = min(WIDTH - col, 4); - int num_row = min(HEIGHT - row, 4); - if(num_row == 4) { - const uchar8 rgbs0_0 = vload8(0, rgb + bgri_start); - const uchar8 rgbs0_1 = vload8(0, rgb + bgri_start + 8); - const uchar8 rgbs1_0 = vload8(0, rgb + bgri_start + RGB_STRIDE); - const uchar8 rgbs1_1 = vload8(0, rgb + bgri_start + RGB_STRIDE + 8); - const uchar8 rgbs2_0 = vload8(0, rgb + bgri_start + RGB_STRIDE * 2); - const uchar8 rgbs2_1 = vload8(0, rgb + bgri_start + RGB_STRIDE * 2 + 8); - const uchar8 rgbs3_0 = vload8(0, rgb + bgri_start + RGB_STRIDE * 3); - const uchar8 rgbs3_1 = vload8(0, rgb + bgri_start + RGB_STRIDE * 3 + 8); - if(num_col == 4) { - convert_4_ys(out_yuv, yi_start, rgbs0_0, rgbs0_1); - convert_4_ys(out_yuv, yi_start + WIDTH, rgbs1_0, rgbs1_1); - convert_4_ys(out_yuv, yi_start + WIDTH * 2, rgbs2_0, rgbs2_1); - convert_4_ys(out_yuv, yi_start + WIDTH * 3, rgbs3_0, rgbs3_1); - convert_2_uvs(out_yuv, uvi, rgbs0_0, rgbs1_0, rgbs0_1, rgbs1_1); - convert_2_uvs(out_yuv, uvi + WIDTH, rgbs2_0, rgbs3_0, rgbs2_1, rgbs3_1); - } else if(num_col == 2) { - convert_2_ys(out_yuv, yi_start, rgbs0_0); - convert_2_ys(out_yuv, yi_start + WIDTH, rgbs1_0); - convert_2_ys(out_yuv, yi_start + WIDTH * 2, rgbs2_0); - convert_2_ys(out_yuv, yi_start + WIDTH * 3, rgbs3_0); - convert_uv(out_yuv, uvi, rgbs0_0, rgbs1_0); - convert_uv(out_yuv, uvi + WIDTH, rgbs2_0, rgbs3_0); - } - } else { - const uchar8 rgbs0_0 = vload8(0, rgb + bgri_start); - const uchar8 rgbs0_1 = vload8(0, rgb + bgri_start + 8); - const uchar8 rgbs1_0 = vload8(0, rgb + bgri_start + RGB_STRIDE); - const uchar8 rgbs1_1 = vload8(0, rgb + bgri_start + RGB_STRIDE + 8); - if(num_col == 4) { - convert_4_ys(out_yuv, yi_start, rgbs0_0, rgbs0_1); - convert_4_ys(out_yuv, yi_start + WIDTH, rgbs1_0, rgbs1_1); - convert_2_uvs(out_yuv, uvi, rgbs0_0, rgbs1_0, rgbs0_1, rgbs1_1); - } else if(num_col == 2) { - convert_2_ys(out_yuv, yi_start, rgbs0_0); - convert_2_ys(out_yuv, yi_start + WIDTH, rgbs1_0); - convert_uv(out_yuv, uvi, rgbs0_0, rgbs1_0); - } - } -} diff --git a/tools/sim/tests/test_metadrive_bridge.py b/tools/sim/tests/test_metadrive_bridge.py index 04ce5d584f..9be640d736 100644 --- a/tools/sim/tests/test_metadrive_bridge.py +++ b/tools/sim/tests/test_metadrive_bridge.py @@ -8,7 +8,6 @@ from openpilot.tools.sim.bridge.metadrive.metadrive_bridge import MetaDriveBridg from openpilot.tools.sim.tests.test_sim_bridge import TestSimBridgeBase @pytest.mark.slow -@pytest.mark.filterwarnings("ignore::pyopencl.CompilerWarning") # Unimportant warning of non-empty compile log class TestMetaDriveBridge(TestSimBridgeBase): @pytest.fixture(autouse=True) def setup_create_bridge(self, test_duration): diff --git a/tools/tuning/measure_steering_accuracy.py b/tools/tuning/measure_steering_accuracy.py index e4aef0ba15..ae3344c2eb 100755 --- a/tools/tuning/measure_steering_accuracy.py +++ b/tools/tuning/measure_steering_accuracy.py @@ -117,12 +117,8 @@ if __name__ == "__main__": parser.add_argument('--route', help="route name") parser.add_argument('--addr', default='127.0.0.1', help="IP address for optional ZMQ listener, default to msgq") parser.add_argument('--group', default='all', help="speed group to display, [crawl|slow|medium|fast|veryfast|germany|all], default to all") - parser.add_argument('--cache', default=False, action='store_true', help="use cached data, default to False") args = parser.parse_args() - if args.cache: - os.environ['FILEREADER_CACHE'] = '1' - tool = SteeringAccuracyTool(args) if args.route is not None: diff --git a/uv.lock b/uv.lock index 674b18b7ea..782c20849c 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.11, <3.13" -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version < '3.12'", -] +requires-python = ">=3.12.3, <3.13" [[package]] name = "aiohappyeyeballs" @@ -30,23 +26,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, - { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, - { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, - { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, - { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, - { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, - { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, - { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, - { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, - { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, @@ -132,12 +111,6 @@ version = "13.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0c/9d/486d31e76784cc0ad943f420c5e05867263b32b37e2f4b0f7f22fdc1ca3a/av-13.1.0.tar.gz", hash = "sha256:d3da736c55847d8596eb8c26c60e036f193001db3bc5c10da8665622d906c17e", size = 3957908, upload-time = "2024-10-06T04:54:57.507Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/54/c4227080c9700384db90072ace70d89b6a288b3748bd2ec0e32580a49e7f/av-13.1.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:867385e6701464a5c95903e24d2e0df1c7e0dbf211ed91d0ce639cd687373e10", size = 24255112, upload-time = "2024-10-06T04:52:48.49Z" }, - { url = "https://files.pythonhosted.org/packages/32/4a/eb9348231655ca99b200b380f4edbceff7358c927a285badcc84b18fb1c9/av-13.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb7a3f319401a46b0017771268ff4928501e77cf00b1a2aa0721e20b2fd1146e", size = 19467930, upload-time = "2024-10-06T04:52:52.118Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/48c80252bdbc3a75a54dd205a7fab8f613914009b9e5416202757208e040/av-13.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad904f860147bceaca65b0d3174a8153f35c570d465161d210f1879970b15559", size = 32207671, upload-time = "2024-10-06T04:52:55.82Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/3332c7fa8c43b65680a94f279ea3e832b5500de3a1392bac6112881e984b/av-13.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a906e017b29d0eb80d9ccf7a98d19268122da792dbb68eb741cfebba156e6aed", size = 31520911, upload-time = "2024-10-06T04:52:59.231Z" }, - { url = "https://files.pythonhosted.org/packages/e5/bb/2e03acb9b27591d97f700a3a6c27cfd1bc53fa148177747eda8a70cca1e9/av-13.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ce894d7847897da7be63277a0875bd93c51327134ac226c67978de014c7979f", size = 34048399, upload-time = "2024-10-06T04:53:03.934Z" }, - { url = "https://files.pythonhosted.org/packages/85/44/527aa3b65947d42cfe829326026edf0cd1a8c459390076034be275616c36/av-13.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:384bcdb5fc3238a263a5a25cc9efc690859fa4148cc4b07e00fae927178db22a", size = 25779569, upload-time = "2024-10-06T04:53:07.582Z" }, { url = "https://files.pythonhosted.org/packages/9b/aa/4bdd8ce59173574fc6e0c282c71ee6f96fca82643d97bf172bc4cb5a5674/av-13.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:261dbc3f4b55f4f8f3375b10b2258fca7f2ab7a6365c01bc65e77a0d5327a195", size = 24268674, upload-time = "2024-10-06T04:53:11.251Z" }, { url = "https://files.pythonhosted.org/packages/17/b4/b267dd5bad99eed49ec6731827c6bcb5ab03864bf732a7ebb81e3df79911/av-13.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83d259ef86b9054eb914bc7c6a7f6092a6d75cb939295e70ee979cfd92a67b99", size = 19475617, upload-time = "2024-10-06T04:53:13.832Z" }, { url = "https://files.pythonhosted.org/packages/68/32/4209e51f54d7b54a1feb576d309c671ed1ff437b54fcc4ec68c239199e0a/av-13.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b4d3ca159eceab97e3c0fb08fe756520fb95508417f76e48198fda2a5b0806", size = 32468873, upload-time = "2024-10-06T04:53:17.639Z" }, @@ -146,50 +119,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/ff/48fa68888b8d5bae36d915556ff18f9e5fdc6b5ff5ae23dc4904c9713168/av-13.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:5ea0deab0e6a739cb742fba2a3983d8102f7516a3cdf3c46669f3cac0ed1f351", size = 25781343, upload-time = "2024-10-06T04:53:29.577Z" }, ] -[[package]] -name = "azure-core" -version = "1.38.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/1b/e503e08e755ea94e7d3419c9242315f888fc664211c90d032e40479022bf/azure_core-1.38.0.tar.gz", hash = "sha256:8194d2682245a3e4e3151a667c686464c3786fed7918b394d035bdcd61bb5993", size = 363033, upload-time = "2026-01-12T17:03:05.535Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335", size = 217825, upload-time = "2026-01-12T17:03:07.291Z" }, -] - -[[package]] -name = "azure-identity" -version = "1.25.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "cryptography" }, - { name = "msal" }, - { name = "msal-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/8d/1a6c41c28a37eab26dc85ab6c86992c700cd3f4a597d9ed174b0e9c69489/azure_identity-1.25.1.tar.gz", hash = "sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456", size = 279826, upload-time = "2025-10-06T20:30:02.194Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/7b/5652771e24fff12da9dde4c20ecf4682e606b104f26419d139758cc935a6/azure_identity-1.25.1-py3-none-any.whl", hash = "sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651", size = 191317, upload-time = "2025-10-06T20:30:04.251Z" }, -] - -[[package]] -name = "azure-storage-blob" -version = "12.28.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "cryptography" }, - { name = "isodate" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/24/072ba8e27b0e2d8fec401e9969b429d4f5fc4c8d4f0f05f4661e11f7234a/azure_storage_blob-12.28.0.tar.gz", hash = "sha256:e7d98ea108258d29aa0efbfd591b2e2075fa1722a2fae8699f0b3c9de11eff41", size = 604225, upload-time = "2026-01-06T23:48:57.282Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/3a/6ef2047a072e54e1142718d433d50e9514c999a58f51abfff7902f3a72f8/azure_storage_blob-12.28.0-py3-none-any.whl", hash = "sha256:00fb1db28bf6a7b7ecaa48e3b1d5c83bfadacc5a678b77826081304bd87d6461", size = 431499, upload-time = "2026-01-06T23:48:58.995Z" }, -] - [[package]] name = "casadi" version = "3.7.2" @@ -199,12 +128,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/92/62/1e98662024915ecb09c6894c26a3f497f4afa66570af3f53db4651fc45f1/casadi-3.7.2.tar.gz", hash = "sha256:b4d7bd8acdc4180306903ae1c9eddaf41be2a3ae2fa7154c57174ae64acdc60d", size = 6053600, upload-time = "2025-09-10T10:05:49.521Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/01/d5e3058775ec8e24a01eb74d36099493b872536ef9e39f1e49624b977778/casadi-3.7.2-cp311-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:f43b0562d05a5e6e81f1885fc4ae426c382e36eebfd8d27f1baff6052178a9b0", size = 47115880, upload-time = "2025-09-10T07:52:24.399Z" }, - { url = "https://files.pythonhosted.org/packages/0e/cf/4af27e010d599a5419129d34fdde41637029a1cca2a40bef0965d6d52228/casadi-3.7.2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:70add3334b437b60a9bc0f864d094350f1a4fcbf9e8bafec870b61aed64674df", size = 42293337, upload-time = "2025-09-10T08:03:32.556Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4c/d1a50cc840103e00effcbaf8e911b6b3fb6ba2c8f4025466f524854968ed/casadi-3.7.2-cp311-none-manylinux2014_aarch64.whl", hash = "sha256:392d3367a4b33cf223013dad8122a0e549da40b1702a5375f82f85b563e5c0cf", size = 47277175, upload-time = "2025-09-10T08:04:08.811Z" }, - { url = "https://files.pythonhosted.org/packages/be/29/6e5714d124e6ddafbccc3ed774ca603081caa1175c7f0e1c52484184dfb3/casadi-3.7.2-cp311-none-manylinux2014_i686.whl", hash = "sha256:2ce09e0ced6df33048dccd582b5cfa2c9ff5193b12858b2584078afc17761905", size = 72438460, upload-time = "2025-09-10T08:05:02.769Z" }, - { url = "https://files.pythonhosted.org/packages/23/32/ac1f3999273aa4aae48516f6f4b7b267e0cc70d8527866989798cb81312f/casadi-3.7.2-cp311-none-manylinux2014_x86_64.whl", hash = "sha256:5086799a46d10ba884b72fd02c21be09dae52cbc189272354a5d424791b55f37", size = 75574474, upload-time = "2025-09-10T08:06:00.709Z" }, - { url = "https://files.pythonhosted.org/packages/68/78/7fd10709504c1757f70db3893870a891fcb9f1ec9f05e8ef2e3f3b9d7e2f/casadi-3.7.2-cp311-none-win_amd64.whl", hash = "sha256:72aa5727417d781ed216f16b5e93c6ddca5db27d83b0015a729e8ad570cdc465", size = 50994144, upload-time = "2025-09-10T08:06:42.384Z" }, { url = "https://files.pythonhosted.org/packages/65/c8/689d085447b1966f42bdb8aa4fbebef49a09697dbee32ab02a865c17ac1b/casadi-3.7.2-cp312-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:309ea41a69c9230390d349b0dd899c6a19504d1904c0756bef463e47fb5c8f9a", size = 47116756, upload-time = "2025-09-10T07:53:00.931Z" }, { url = "https://files.pythonhosted.org/packages/1e/c0/3c4704394a6fd4dfb2123a4fd71ba64a001f340670a3eba45be7a19ac736/casadi-3.7.2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6033381234db810b2247d16c6352e679a009ec4365d04008fc768866e011ed58", size = 42293718, upload-time = "2025-09-10T08:07:16.415Z" }, { url = "https://files.pythonhosted.org/packages/f3/24/4cf05469ddf8544da5e92f359f96d716a97e7482999f085a632bc4ef344a/casadi-3.7.2-cp312-none-manylinux2014_aarch64.whl", hash = "sha256:732f2804d0766454bb75596339e4f2da6662ffb669621da0f630ed4af9e83d6a", size = 47276175, upload-time = "2025-09-10T08:08:09.29Z" }, @@ -231,19 +154,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, @@ -264,22 +174,6 @@ version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, @@ -347,17 +241,6 @@ dependencies = [ ] 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/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" }, @@ -369,46 +252,30 @@ wheels = [ { 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]] name = "coverage" -version = "7.13.1" +version = "7.13.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, - { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, - { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, - { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, - { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, - { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, - { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, - { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, - { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, - { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, - { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] [[package]] @@ -425,10 +292,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/ba/501ef1b02119402cf1a31c01eb2cb8399660bca863c2f4dd3dc060220284/crcmod_plus-2.3.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9166bc3c9b5e7b07b4e6854cac392b4a451b31d58d3950e48c140ab7b5d05394", size = 27135, upload-time = "2025-10-10T22:13:51.889Z" }, { url = "https://files.pythonhosted.org/packages/49/90/d4556c9db69c83e726c5b88da3d656fdaac7d60c4d27b43cb939bed80069/crcmod_plus-2.3.1-cp311-abi3-win32.whl", hash = "sha256:cb99b694cce5c862560cf332a8b5e793620e28f0de3726995608bbd6f9b6e09a", size = 22384, upload-time = "2025-10-10T22:13:53.016Z" }, { url = "https://files.pythonhosted.org/packages/4d/7e/57bb97a8c7b4e19900744f58b67dc83bc9c83aaac670deeede9fb3bfab6a/crcmod_plus-2.3.1-cp311-abi3-win_amd64.whl", hash = "sha256:82b0f7e968c430c5a80fe0fc59e75cb54f2e84df2ed0cee5a3ff9cadfbf8a220", size = 22912, upload-time = "2025-10-10T22:13:53.849Z" }, - { url = "https://files.pythonhosted.org/packages/76/66/419ae3991bb68943cb752e2f4d317c555e3f02a298dd498f26113874ee59/crcmod_plus-2.3.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9397324da1be2729f894744d9031a21ed97584c17fb0289e69e0c3c60916fc5f", size = 19880, upload-time = "2025-10-10T22:14:17.269Z" }, - { url = "https://files.pythonhosted.org/packages/18/f0/d10c9b859927b2cdc38eafc33c8b66e4ede02eaa174df4575681dab5a0f1/crcmod_plus-2.3.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:073c7a3b832652e66c41c8b8705eaecda704d1cbe850b9fa05fdee36cd50745a", size = 21120, upload-time = "2025-10-10T22:14:18.117Z" }, - { url = "https://files.pythonhosted.org/packages/6c/68/cbd8f1707b37b80f9a0bf643e04747b0196f69cf065b52ed56639afbecef/crcmod_plus-2.3.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e5f4c62553f772ea7ae12d9484801b752622c9c288e49ee7ea34a20b94e4920", size = 21698, upload-time = "2025-10-10T22:14:20.044Z" }, - { url = "https://files.pythonhosted.org/packages/41/1b/4ab1681ecbfc48d7e4641fb178c97374eb475ae4109255bdd832110cbbe2/crcmod_plus-2.3.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5e80a9860f66f339956f540d86a768f4fe8c8bfcb139811f14be864425c48d64", size = 23289, upload-time = "2025-10-10T22:14:20.875Z" }, ] [[package]] @@ -475,10 +338,6 @@ version = "3.2.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/91/85/7574c9cd44b69a27210444b6650f6477f56c75fee1b70d7672d3e4166167/cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6", size = 3280291, upload-time = "2026-01-04T14:14:14.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/cc/8f06145ec3efa121c8b1b67f06a640386ddacd77ee3e574da582a21b14ee/cython-3.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff9af2134c05e3734064808db95b4dd7341a39af06e8945d05ea358e1741aaed", size = 2953769, upload-time = "2026-01-04T14:15:00.361Z" }, - { url = "https://files.pythonhosted.org/packages/55/b0/706cf830eddd831666208af1b3058c2e0758ae157590909c1f634b53bed9/cython-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67922c9de058a0bfb72d2e75222c52d09395614108c68a76d9800f150296ddb3", size = 3243841, upload-time = "2026-01-04T14:15:02.066Z" }, - { url = "https://files.pythonhosted.org/packages/ac/25/58893afd4ef45f79e3d4db82742fa4ff874b936d67a83c92939053920ccd/cython-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b362819d155fff1482575e804e43e3a8825332d32baa15245f4642022664a3f4", size = 3378083, upload-time = "2026-01-04T14:15:04.248Z" }, - { url = "https://files.pythonhosted.org/packages/32/e4/424a004d7c0d8a4050c81846ebbd22272ececfa9a498cb340aa44fccbec2/cython-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a64a112a34ec719b47c01395647e54fb4cf088a511613f9a3a5196694e8e382", size = 2769990, upload-time = "2026-01-04T14:15:06.53Z" }, { url = "https://files.pythonhosted.org/packages/91/4d/1eb0c7c196a136b1926f4d7f0492a96c6fabd604d77e6cd43b56a3a16d83/cython-3.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64d7f71be3dd6d6d4a4c575bb3a4674ea06d1e1e5e4cd1b9882a2bc40ed3c4c9", size = 2970064, upload-time = "2026-01-04T14:15:08.567Z" }, { url = "https://files.pythonhosted.org/packages/03/1c/46e34b08bea19a1cdd1e938a4c123e6299241074642db9d81983cef95e9f/cython-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:869487ea41d004f8b92171f42271fbfadb1ec03bede3158705d16cd570d6b891", size = 3226757, upload-time = "2026-01-04T14:15:10.812Z" }, { url = "https://files.pythonhosted.org/packages/12/33/3298a44d201c45bcf0d769659725ae70e9c6c42adf8032f6d89c8241098d/cython-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55b6c44cd30821f0b25220ceba6fe636ede48981d2a41b9bbfe3c7902ce44ea7", size = 3388969, upload-time = "2026-01-04T14:15:12.45Z" }, @@ -500,10 +359,6 @@ name = "dearpygui" version = "2.1.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/b5/d5bae262633d05f82aeeb3f84ae6240fe3f2955ab6fb4509ebf8048a0b9e/dearpygui-2.1.1-cp311-cp311-macosx_10_6_x86_64.whl", hash = "sha256:8a7c8608b365f4b380b7326679023595fecd04b78c514f2cfd349b0a1108bd0e", size = 2100934, upload-time = "2025-11-14T14:47:38.172Z" }, - { url = "https://files.pythonhosted.org/packages/93/86/75fa9a0f0a7b4b62810cc6f1e8ebaea3df0a825c0adf27d2024aaac2d178/dearpygui-2.1.1-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:ee87153fdd494ccead8c2345553acd7a9ee61c031e3223f4160aa560709248a3", size = 1895473, upload-time = "2025-11-14T14:47:47.064Z" }, - { url = "https://files.pythonhosted.org/packages/ab/7a/e109e06f8f4379d41a4e672c49aba42e7fcf0eec88056fa06185f4e52c98/dearpygui-2.1.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:964fbb3735017b919efa58104b2d7e9b84a168ff5c1031ae0652d5bc0a48bf5b", size = 2640408, upload-time = "2025-11-14T14:47:53.124Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b5/2ec29d9b47c30ecee96c6f6a0cf229f2898ce3e133a1a0e5b0cd5db82e6b/dearpygui-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:6141184ff59fa4b8df1b81b077cb8cc2b2ef9c0ff92e69c6063062b6d251f426", size = 1808736, upload-time = "2025-11-14T14:47:26.46Z" }, { url = "https://files.pythonhosted.org/packages/79/41/2146e8d03d28b5a66d5282beb26ffd9ab68a729a29d31e2fe91809271bf5/dearpygui-2.1.1-cp312-cp312-macosx_10_6_x86_64.whl", hash = "sha256:238aea7b4be7376f564dae8edd563b280ec1483a03786022969938507691e017", size = 2101529, upload-time = "2025-11-14T14:47:39.646Z" }, { url = "https://files.pythonhosted.org/packages/b0/c5/fcc37ef834fe225241aa4f18d77aaa2903134f283077978d65a901c624c6/dearpygui-2.1.1-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:c27ca6ecd4913555b717f3bb341c0b6a27d6c9fdc9932f0b3c31ae2ef893ae35", size = 1895555, upload-time = "2025-11-14T14:47:48.149Z" }, { url = "https://files.pythonhosted.org/packages/74/66/19f454ba02d5f03a847cc1dfee4a849cd2307d97add5ba26fecdca318adb/dearpygui-2.1.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:8c071e9c165d89217bdcdaf769c6069252fcaee50bf369489add524107932273", size = 2641509, upload-time = "2025-11-14T14:47:54.581Z" }, @@ -560,11 +415,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.3" +version = "3.24.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/a8/dae62680be63cbb3ff87cfa2f51cf766269514ea5488479d42fec5aa6f3a/filelock-3.24.2.tar.gz", hash = "sha256:c22803117490f156e59fafce621f0550a7a853e2bbf4f87f112b11d469b6c81b", size = 37601, upload-time = "2026-02-16T02:50:45.614Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, + { url = "https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl", hash = "sha256:667d7dc0b7d1e1064dd5f8f8e80bdac157a6482e8d2e02cd16fd3b6b33bd6556", size = 24152, upload-time = "2026-02-16T02:50:44Z" }, ] [[package]] @@ -573,14 +428,6 @@ version = "4.61.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, - { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, - { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, - { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, - { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, - { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, - { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, - { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, @@ -598,22 +445,6 @@ version = "1.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, - { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, - { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, - { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, - { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, - { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, - { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, - { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, - { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, @@ -633,15 +464,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] -[[package]] -name = "future-fstrings" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5d/e2/3874574cce18a2e3608abfe5b4b5b3c9765653c464f5da18df8971cf501d/future_fstrings-1.2.0.tar.gz", hash = "sha256:6cf41cbe97c398ab5a81168ce0dbb8ad95862d3caf23c21e4430627b90844089", size = 5786, upload-time = "2019-06-16T03:04:42.651Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/6d/ea1d52e9038558dd37f5d30647eb9f07888c164960a5d4daa5f970c6da25/future_fstrings-1.2.0-py2.py3-none-any.whl", hash = "sha256:90e49598b553d8746c4dc7d9442e0359d038c3039d802c91c0a55505da318c63", size = 6138, upload-time = "2019-06-16T03:04:40.395Z" }, -] - [[package]] name = "ghp-import" version = "2.1.0" @@ -660,18 +482,11 @@ version = "1.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, - { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, - { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, - { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, - { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, ] [[package]] @@ -738,15 +553,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/94/040a0d9c81f018c39bd887b7b825013b024deb0a6c795f9524797e2cd41b/inputs-0.5-py2.py3-none-any.whl", hash = "sha256:13f894564e52134cf1e3862b1811da034875eb1f2b62e6021e3776e9669a96ec", size = 33630, upload-time = "2018-10-05T22:38:28.28Z" }, ] -[[package]] -name = "isodate" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, -] - [[package]] name = "jeepney" version = "0.9.0" @@ -777,34 +583,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/9e/820c4b086ad01ba7d77369fb8b11470a01fac9b4977f02e18659cf378b6b/json_rpc-1.15.0-py2.py3-none-any.whl", hash = "sha256:4a4668bbbe7116feb4abbd0f54e64a4adcf4b8f648f19ffa0848ad0f6606a9bf", size = 39450, upload-time = "2023-06-11T09:45:47.136Z" }, ] -[[package]] -name = "kaitaistruct" -version = "0.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/27/b8/ca7319556912f68832daa4b81425314857ec08dfccd8dbc8c0f65c992108/kaitaistruct-0.11.tar.gz", hash = "sha256:053ee764288e78b8e53acf748e9733268acbd579b8d82a427b1805453625d74b", size = 11519, upload-time = "2025-09-08T15:46:25.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/4a/cf14bf3b1f5ffb13c69cf5f0ea78031247790558ee88984a8bdd22fae60d/kaitaistruct-0.11-py2.py3-none-any.whl", hash = "sha256:5c6ce79177b4e193a577ecd359e26516d1d6d000a0bffd6e1010f2a46a62a561", size = 11372, upload-time = "2025-09-08T15:46:23.635Z" }, -] - [[package]] name = "kiwisolver" version = "1.4.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, - { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, - { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, - { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, - { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, - { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, @@ -818,11 +602,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, - { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, - { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, ] [[package]] @@ -842,22 +621,6 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, - { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, - { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, - { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, - { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, - { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, - { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, - { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, - { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, - { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, - { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, - { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, @@ -876,12 +639,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, - { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, - { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, - { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, - { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, - { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, ] [[package]] @@ -893,15 +650,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/bc/7b/bbf6b00488662be5d2eb7a188222c264b6f713bac10dc4a77bf37a4cb4b6/mapbox_earcut-2.0.0.tar.gz", hash = "sha256:81eab6b86cf99551deb698b98e3f7502c57900e5c479df15e1bdaf1a57f0f9d6", size = 39934, upload-time = "2025-11-16T18:41:27.251Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/9f/fbd15d9e348e75e986d6912c4eab99888106b7e5fb0a01e765422f7cd464/mapbox_earcut-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:9b5040e79e3783295e99c90277f31c1cbaddd3335297275331995ba5680e3649", size = 55773, upload-time = "2025-11-16T18:40:20.045Z" }, - { url = "https://files.pythonhosted.org/packages/72/40/be761298704fbbaa81c5618bb306f1510fb068e482f6a1c8b3b6c1b31479/mapbox_earcut-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf43baafec3ef1e967319d9b5da96bc6ddf3dbb204b6f3535275eda4b519a72", size = 52444, upload-time = "2025-11-16T18:40:21.501Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0b/0c0c08db9663238ffb82c48259582dc0047a3255d98c0ac83c48026b7544/mapbox_earcut-2.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a283531847f603dd9d69afb75b21bd009d385ca9485fcd3e5a7fa5db1ccd913", size = 56803, upload-time = "2025-11-16T18:40:22.891Z" }, - { url = "https://files.pythonhosted.org/packages/f0/4a/86796859383d7d11fa5d4bcf1983f94c6cbb9eeb60fb3bab527fec4b32fa/mapbox_earcut-2.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab697676f4cec4572d4e941b7a3429a6687bf2ac6e8db3f3781024e3239ae3a0", size = 59403, upload-time = "2025-11-16T18:40:24.021Z" }, - { url = "https://files.pythonhosted.org/packages/6c/db/adaf981ab3bcfcf993ef317636b1f27210d6834bb1e8d63db6ad7c08214a/mapbox_earcut-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1bdac76e048f4299accf4eaf797079ddfc330442e7231c15535ed198100d6c5", size = 152876, upload-time = "2025-11-16T18:40:25.588Z" }, - { url = "https://files.pythonhosted.org/packages/d2/83/86417974039e7554c9e1e55c852a7e9c2a1390d64675eb85d70e5fa7eb37/mapbox_earcut-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a6945b23f859bef11ce3194303d17bd371c86b637e7029f81b1feaff3db3758", size = 157548, upload-time = "2025-11-16T18:40:27.202Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4c/c82a292bb21e5c651d81334123db2d654c5c9d19b2197080d3429dc1e49a/mapbox_earcut-2.0.0-cp311-cp311-win32.whl", hash = "sha256:8e119524c29406afb5eaa15e933f297d35679293a3ca62ced22f97a14c484cb5", size = 51424, upload-time = "2025-11-16T18:40:28.415Z" }, - { url = "https://files.pythonhosted.org/packages/30/57/6c39d7db81f72a3e4814ef152c8fb8dfe275dc4b03c9bfa073d251e3755f/mapbox_earcut-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:378bbbb3304e446023752db8f44ecd6e7ef965bcbda36541d2ae64442ba94254", size = 56662, upload-time = "2025-11-16T18:40:29.863Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d6/a1ef6e196b3d6968bf6546d4f7e54c559f9cff8991fdb880df0ba1618f52/mapbox_earcut-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:6d249a431abd6bbff36f1fd0493247a86de962244cc4081b4d5050b02ed48fb1", size = 50505, upload-time = "2025-11-16T18:40:30.992Z" }, { url = "https://files.pythonhosted.org/packages/8d/93/846804029d955c3c841d8efff77c2b0e8d9aab057d3a077dc8e3f88b5ea4/mapbox_earcut-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db55ce18e698bc9d90914ee7d4f8c3e4d23827456ece7c5d7a1ec91e90c7122b", size = 55623, upload-time = "2025-11-16T18:40:32.113Z" }, { url = "https://files.pythonhosted.org/packages/d3/f6/cc9ece104bc3876b350dba6fef7f34fb7b20ecc028d2cdbdbecb436b1ed1/mapbox_earcut-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01dd6099d16123baf582a11b2bd1d59ce848498cf0cdca3812fd1f8b20ff33b7", size = 52028, upload-time = "2025-11-16T18:40:33.516Z" }, { url = "https://files.pythonhosted.org/packages/88/6e/230da4aabcc56c99e9bddb4c43ce7d4ba3609c0caf2d316fb26535d7c60c/mapbox_earcut-2.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d5a098aae26a52282bc981a38e7bf6b889d2ea7442f2cd1903d2ba842f4ff07", size = 56351, upload-time = "2025-11-16T18:40:35.217Z" }, @@ -915,11 +663,11 @@ wheels = [ [[package]] name = "markdown" -version = "3.10" +version = "3.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] [[package]] @@ -928,17 +676,6 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, @@ -969,13 +706,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, - { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, @@ -983,9 +713,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, - { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, - { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, - { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, ] [[package]] @@ -1098,11 +825,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, - { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, - { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, - { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, @@ -1130,121 +852,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] -[[package]] -name = "msal" -version = "1.34.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/0e/c857c46d653e104019a84f22d4494f2119b4fe9f896c92b4b864b3b045cc/msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f", size = 153961, upload-time = "2025-09-22T23:05:48.989Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/dc/18d48843499e278538890dc709e9ee3dea8375f8be8e82682851df1b48b5/msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1", size = 116987, upload-time = "2025-09-22T23:05:47.294Z" }, -] - -[[package]] -name = "msal-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "msal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, -] - [[package]] name = "multidict" -version = "6.7.0" +version = "6.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, - { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, - { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, - { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, - { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, - { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, - { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, - { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, - { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, - { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, - { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, - { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, - { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, - { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, - { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, - { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, - { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, - { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, -] - -[[package]] -name = "natsort" -version = "8.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload-time = "2023-06-20T04:17:19.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] [[package]] name = "numpy" -version = "2.4.1" +version = "2.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" }, - { url = "https://files.pythonhosted.org/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" }, - { url = "https://files.pythonhosted.org/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" }, - { url = "https://files.pythonhosted.org/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" }, - { url = "https://files.pythonhosted.org/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" }, - { url = "https://files.pythonhosted.org/packages/6a/93/098e1162ae7522fc9b618d6272b77404c4656c72432ecee3abc029aa3de0/numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", size = 6236575, upload-time = "2026-01-10T06:42:33.872Z" }, - { url = "https://files.pythonhosted.org/packages/8c/de/f5e79650d23d9e12f38a7bc6b03ea0835b9575494f8ec94c11c6e773b1b1/numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", size = 12604479, upload-time = "2026-01-10T06:42:35.778Z" }, - { url = "https://files.pythonhosted.org/packages/dd/65/e1097a7047cff12ce3369bd003811516b20ba1078dbdec135e1cd7c16c56/numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", size = 10578325, upload-time = "2026-01-10T06:42:38.518Z" }, - { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" }, - { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" }, - { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" }, - { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" }, - { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" }, - { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" }, - { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" }, - { url = "https://files.pythonhosted.org/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" }, - { url = "https://files.pythonhosted.org/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" }, - { url = "https://files.pythonhosted.org/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" }, - { url = "https://files.pythonhosted.org/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" }, - { url = "https://files.pythonhosted.org/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c7/b801bf98514b6ae6475e941ac05c58e6411dd863ea92916bfd6d510b08c1/numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", size = 12492579, upload-time = "2026-01-10T06:44:57.094Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, ] [[package]] @@ -1259,12 +910,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/3b/8a/335c03a8683a88a32f9a6bb98899ea6df241a41df64b37b9696772414794/onnx-1.20.1.tar.gz", hash = "sha256:ded16de1df563d51fbc1ad885f2a426f814039d8b5f4feb77febe09c0295ad67", size = 12048980, upload-time = "2026-01-10T01:40:03.043Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/38/1a0e74d586c08833404100f5c052f92732fb5be417c0b2d7cb0838443bfe/onnx-1.20.1-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:53426e1b458641e7a537e9f176330012ff59d90206cac1c1a9d03cdd73ed3095", size = 17904965, upload-time = "2026-01-10T01:39:13.532Z" }, - { url = "https://files.pythonhosted.org/packages/96/25/64b076e9684d17335f80b15b3bf502f7a8e1a89f08a6b208d4f2861b3011/onnx-1.20.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca7281f8c576adf396c338cf43fff26faee8d4d2e2577b8e73738f37ceccf945", size = 17415179, upload-time = "2026-01-10T01:39:16.516Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d5/6743b409421ced20ad5af1b3a7b4c4e568689ffaca86db431692fca409a6/onnx-1.20.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2297f428c51c7fc6d8fad0cf34384284dfeff3f86799f8e83ef905451348ade0", size = 17513672, upload-time = "2026-01-10T01:39:19.35Z" }, - { url = "https://files.pythonhosted.org/packages/9a/6b/dae82e6fdb2043302f29adca37522312ea2be55b75907b59be06fbdffe87/onnx-1.20.1-cp311-cp311-win32.whl", hash = "sha256:63d9cbcab8c96841eadeb7c930e07bfab4dde8081eb76fb68e0dfb222706b81e", size = 16239336, upload-time = "2026-01-10T01:39:22.506Z" }, - { url = "https://files.pythonhosted.org/packages/8e/17/a0d7863390c1f2067d7c02dcc1477034965c32aaa1407bfcf775305ffee4/onnx-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:d78cde72d7ca8356a2d99c5dc0dbf67264254828cae2c5780184486c0cd7b3bf", size = 16392120, upload-time = "2026-01-10T01:39:25.106Z" }, - { url = "https://files.pythonhosted.org/packages/aa/72/9b879a46eb7a3322223791f36bf9c25d95da9ed93779eabb75a560f22e5b/onnx-1.20.1-cp311-cp311-win_arm64.whl", hash = "sha256:0104bb2d4394c179bcea3df7599a45a2932b80f4633840896fcf0d7d8daecea2", size = 16346923, upload-time = "2026-01-10T01:39:27.782Z" }, { url = "https://files.pythonhosted.org/packages/7c/4c/4b17e82f91ab9aa07ff595771e935ca73547b035030dc5f5a76e63fbfea9/onnx-1.20.1-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:1d923bb4f0ce1b24c6859222a7e6b2f123e7bfe7623683662805f2e7b9e95af2", size = 17903547, upload-time = "2026-01-10T01:39:31.015Z" }, { url = "https://files.pythonhosted.org/packages/64/5e/1bfa100a9cb3f2d3d5f2f05f52f7e60323b0e20bb0abace1ae64dbc88f25/onnx-1.20.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc0b7d8b5a94627dc86c533d5e415af94cbfd103019a582669dad1f56d30281", size = 17412021, upload-time = "2026-01-10T01:39:33.885Z" }, { url = "https://files.pythonhosted.org/packages/fb/71/d3fec0dcf9a7a99e7368112d9c765154e81da70fcba1e3121131a45c245b/onnx-1.20.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9336b6b8e6efcf5c490a845f6afd7e041c89a56199aeda384ed7d58fb953b080", size = 17510450, upload-time = "2026-01-10T01:39:36.589Z" }, @@ -1275,20 +920,20 @@ wheels = [ [[package]] name = "opencv-python-headless" -version = "4.13.0.90" +version = "4.13.0.92" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/76/38c4cbb5ccfce7aaf36fd9be9fc74a15c85a48ef90bfaca2049b486e10c5/opencv_python_headless-4.13.0.90-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:12a28674f215542c9bf93338de1b5bffd76996d32da9acb9e739fdb9c8bbd738", size = 46020414, upload-time = "2026-01-18T09:07:10.801Z" }, - { url = "https://files.pythonhosted.org/packages/93/c5/4b40daa5003b45aa8397f160324a091ed323733e2446dc0bdf3655e77b84/opencv_python_headless-4.13.0.90-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:32255203040dc98803be96362e13f9e4bce20146898222d2e5c242f80de50da5", size = 32568519, upload-time = "2026-01-18T09:07:52.368Z" }, - { url = "https://files.pythonhosted.org/packages/da/65/920e64a7f03cf5917cd2c6a3046293843c1a16ad89f0ed0f1c683979c9de/opencv_python_headless-4.13.0.90-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e13790342591557050157713af17a7435ac1b50c65282715093c9297fa045d8f", size = 35191272, upload-time = "2026-01-18T09:08:49.235Z" }, - { url = "https://files.pythonhosted.org/packages/fc/13/af150685be342dc09bfb0824e2a280020ccf1c7fc64e15a31d9209016aa9/opencv_python_headless-4.13.0.90-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dbc1f4625e5af3a80ebdbd84380227c0f445228588f2521b11af47710caca1ba", size = 57683677, upload-time = "2026-01-18T09:10:23.588Z" }, - { url = "https://files.pythonhosted.org/packages/cd/47/baab2a3b6d8da8c52e73d00207d1ed3155601c2c332ea855455b3fbc8ff4/opencv_python_headless-4.13.0.90-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eba38bc255d0b7d1969c5bcc90a060ca2b61a3403b613872c750bfa5dfe9e03b", size = 36590019, upload-time = "2026-01-18T09:10:49.053Z" }, - { url = "https://files.pythonhosted.org/packages/81/a1/facfe2801a861b424c4221d66e1281cf19735c00e07f063a337a208c11b5/opencv_python_headless-4.13.0.90-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f46b17ea0aa7e4124ca6ad71143f89233ae9557f61d2326bcdb34329a1ddf9bd", size = 62535926, upload-time = "2026-01-18T09:12:47.229Z" }, - { url = "https://files.pythonhosted.org/packages/06/d2/5e9ee7512306c1caa518be929d1f44bb1c189f342f538f73bea6fb94919f/opencv_python_headless-4.13.0.90-cp37-abi3-win32.whl", hash = "sha256:96060fc57a1abb1144b0b8129e2ff3bfcdd0ccd8e8bd05bd85256ff4ed587d3b", size = 30811665, upload-time = "2026-01-18T09:13:44.517Z" }, - { url = "https://files.pythonhosted.org/packages/a0/09/0a4d832448dccd03b2b1bdee70b9fc2e02c147cc7e06975e9cd729569d90/opencv_python_headless-4.13.0.90-cp37-abi3-win_amd64.whl", hash = "sha256:0e0c8c9f620802fddc4fa7f471a1d263c7b0dca16cd9e7e2f996bb8bd2128c0c", size = 40070035, upload-time = "2026-01-18T09:15:14.652Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1e/6f9e38005a6f7f22af785df42a43139d0e20f169eb5787ce8be37ee7fcc9/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c", size = 32568914, upload-time = "2026-02-05T07:01:51.989Z" }, + { url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/33/b5db29a6c00eb8f50708110d8d453747ca125c8b805bc437b289dbdcc057/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e", size = 60391106, upload-time = "2026-02-05T10:30:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c3/52cfea47cd33e53e8c0fbd6e7c800b457245c1fda7d61660b4ffe9596a7f/opencv_python_headless-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:a7cf08e5b191f4ebb530791acc0825a7986e0d0dee2a3c491184bd8599848a4b", size = 30812232, upload-time = "2026-02-05T07:02:29.594Z" }, + { url = "https://files.pythonhosted.org/packages/4a/90/b338326131ccb2aaa3c2c85d00f41822c0050139a4bfe723cfd95455bd2d/opencv_python_headless-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:77a82fe35ddcec0f62c15f2ba8a12ecc2ed4207c17b0902c7a3151ae29f37fb6", size = 40070414, upload-time = "2026-02-05T07:02:26.448Z" }, ] [[package]] @@ -1302,10 +947,9 @@ dependencies = [ { name = "cffi" }, { name = "crcmod-plus" }, { name = "cython" }, - { name = "future-fstrings" }, { name = "inputs" }, + { name = "jeepney" }, { name = "json-rpc" }, - { name = "kaitaistruct" }, { name = "libusb1" }, { name = "mapbox-earcut" }, { name = "numpy" }, @@ -1325,7 +969,6 @@ dependencies = [ { name = "sentry-sdk" }, { name = "setproctitle" }, { name = "setuptools" }, - { name = "smbus2" }, { name = "sounddevice" }, { name = "spidev", marker = "sys_platform == 'linux'" }, { name = "sympy" }, @@ -1338,26 +981,16 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "av" }, - { name = "azure-identity" }, - { name = "azure-storage-blob" }, { name = "dictdiffer" }, - { name = "jeepney" }, { name = "matplotlib" }, { name = "opencv-python-headless" }, { name = "parameterized" }, { name = "pyautogui" }, - { name = "pyopencl" }, - { name = "pyprof2calltree" }, - { name = "pytools", marker = "platform_machine != 'aarch64'" }, { name = "pywinctl" }, - { name = "tabulate" }, - { name = "types-requests" }, - { name = "types-tabulate" }, ] docs = [ { name = "jinja2" }, { name = "mkdocs" }, - { name = "natsort" }, ] testing = [ { name = "codespell" }, @@ -1368,8 +1001,6 @@ testing = [ { name = "pytest-asyncio" }, { name = "pytest-cpp" }, { name = "pytest-mock" }, - { name = "pytest-randomly" }, - { name = "pytest-repeat" }, { name = "pytest-subtests" }, { name = "pytest-timeout" }, { name = "pytest-xdist" }, @@ -1386,8 +1017,6 @@ requires-dist = [ { name = "aiohttp" }, { name = "aiortc" }, { name = "av", marker = "extra == 'dev'" }, - { name = "azure-identity", marker = "extra == 'dev'" }, - { name = "azure-storage-blob", marker = "extra == 'dev'" }, { name = "casadi", specifier = ">=3.6.6" }, { name = "cffi" }, { name = "codespell", marker = "extra == 'testing'" }, @@ -1396,19 +1025,16 @@ requires-dist = [ { name = "cython" }, { name = "dearpygui", marker = "(platform_machine != 'aarch64' and extra == 'tools') or (sys_platform != 'linux' and extra == 'tools')", specifier = ">=2.1.0" }, { name = "dictdiffer", marker = "extra == 'dev'" }, - { name = "future-fstrings" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "inputs" }, - { name = "jeepney", marker = "extra == 'dev'" }, + { name = "jeepney" }, { name = "jinja2", marker = "extra == 'docs'" }, { name = "json-rpc" }, - { name = "kaitaistruct" }, { name = "libusb1" }, { name = "mapbox-earcut" }, { 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 = "natsort", marker = "extra == 'docs'" }, { name = "numpy", specifier = ">=2.0" }, { name = "onnx", specifier = ">=1.14.0" }, { name = "opencv-python-headless", marker = "extra == 'dev'" }, @@ -1420,20 +1046,15 @@ requires-dist = [ { name = "pycapnp", specifier = "==2.1.0" }, { name = "pycryptodome" }, { name = "pyjwt" }, - { name = "pyopencl", marker = "extra == 'dev'" }, { name = "pyopenssl", specifier = "<24.3.0" }, - { name = "pyprof2calltree", marker = "extra == 'dev'" }, { name = "pyserial" }, { name = "pytest", marker = "extra == 'testing'" }, { name = "pytest-asyncio", 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=2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }, - { name = "pytools", marker = "platform_machine != 'aarch64' and extra == 'dev'", specifier = ">=2025.1.6" }, { name = "pywinctl", marker = "extra == 'dev'" }, { name = "pyzmq" }, { name = "qrcode" }, @@ -1444,15 +1065,11 @@ requires-dist = [ { name = "sentry-sdk" }, { name = "setproctitle" }, { name = "setuptools" }, - { name = "smbus2" }, { name = "sounddevice" }, { name = "spidev", marker = "sys_platform == 'linux'" }, { name = "sympy" }, - { name = "tabulate", marker = "extra == 'dev'" }, { name = "tqdm" }, { name = "ty", marker = "extra == 'testing'" }, - { name = "types-requests", marker = "extra == 'dev'" }, - { name = "types-tabulate", marker = "extra == 'dev'" }, { name = "websocket-client" }, { name = "xattr" }, { name = "zstandard" }, @@ -1461,11 +1078,11 @@ provides-extras = ["docs", "testing", "dev", "tools"] [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -1473,11 +1090,6 @@ name = "panda3d" version = "1.10.14" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/9a/31d07e3d7c1b40335e8418c540d63f4d33c571648ed8d69896ab778e65c3/panda3d-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54b8ef9fe3684960a2c7d47b0d63c0354c17bc516795e59db6c1e5bda8c16c1c", size = 67700752, upload-time = "2024-01-08T19:05:55.559Z" }, - { url = "https://files.pythonhosted.org/packages/61/05/fce327535d8907ac01f43813c980f30ea86d37db62c340847519ea2ab222/panda3d-1.10.14-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:93414675894b18eea8d27edc1bbd1dc719eae207d45ec263d47195504bc4705f", size = 118966179, upload-time = "2024-01-08T19:06:03.165Z" }, - { url = "https://files.pythonhosted.org/packages/8a/54/24e205231e7b1bced58ba9620fbec7114673d821fc7ad9ed1804cab556b4/panda3d-1.10.14-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:d1bc0d926f90c8fa14a1587fa9dbe5f89a4eda8c9684fa183a8eaa35fc8e891a", size = 55145295, upload-time = "2024-01-08T19:06:10.319Z" }, - { url = "https://files.pythonhosted.org/packages/06/d3/38e989822292935d7473d35117099f71481cc6b104cb2dd048cb8058a5a8/panda3d-1.10.14-cp311-cp311-win32.whl", hash = "sha256:1039340a4a7965fe4c3e904edb4fff691584df435a154fecccf534587cd07a34", size = 53137177, upload-time = "2024-01-08T19:06:15.901Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/b16c81661ed0d8ad62976004d81845baa321e53314e253ef0841155be770/panda3d-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:1ddf01040b9c5497fb8659e3c5ef793a26c869cfdfb1b75e6d04d6fba0c03b73", size = 64447666, upload-time = "2024-01-08T19:06:22.105Z" }, { url = "https://files.pythonhosted.org/packages/5a/d4/90e98993b1a3f3c9fae83267f8c51186e676a8c1365c4180dfc65cd7ba62/panda3d-1.10.14-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1bfbcee77779f12ecce6a3d5a856e573b25d6343f8c4b107e814d9702e70a65d", size = 67839196, upload-time = "2024-01-08T19:01:00.417Z" }, { url = "https://files.pythonhosted.org/packages/dc/e5/862821575073863ce49cc57b8349b47cb25ce11feae0e419b3d023ac1a69/panda3d-1.10.14-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:bc6540c5559d7e14a8992eff7de0157b7c42406b7ba221941ed224289496841c", size = 119271341, upload-time = "2024-01-08T19:01:09.455Z" }, { url = "https://files.pythonhosted.org/packages/f4/20/f16d91805777825e530037177d9075c83da7384f12b778b133e3164a31f3/panda3d-1.10.14-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:143daab1ce6bedcba711ea3f6cab0ebe5082f22c5f43e7178fadd2dd01209da7", size = 47604077, upload-time = "2024-05-28T20:25:37.118Z" }, @@ -1523,57 +1135,39 @@ wheels = [ [[package]] name = "pathspec" -version = "1.0.3" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "pillow" -version = "12.1.0" +version = "12.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, - { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, - { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, - { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, - { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, - { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, - { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, - { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, - { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, - { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, - { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, - { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, - { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, - { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, - { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, - { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, - { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, - { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, - { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, - { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, ] [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] [[package]] @@ -1609,21 +1203,6 @@ version = "0.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, - { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, - { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, - { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, @@ -1644,33 +1223,33 @@ wheels = [ [[package]] name = "protobuf" -version = "6.33.4" +version = "6.33.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/b8/cda15d9d46d03d4aa3a67cb6bffe05173440ccf86a9541afaf7ac59a1b6b/protobuf-6.33.4.tar.gz", hash = "sha256:dc2e61bca3b10470c1912d166fe0af67bfc20eb55971dcef8dfa48ce14f0ed91", size = 444346, upload-time = "2026-01-12T18:33:40.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/be/24ef9f3095bacdf95b458543334d0c4908ccdaee5130420bf064492c325f/protobuf-6.33.4-cp310-abi3-win32.whl", hash = "sha256:918966612c8232fc6c24c78e1cd89784307f5814ad7506c308ee3cf86662850d", size = 425612, upload-time = "2026-01-12T18:33:29.656Z" }, - { url = "https://files.pythonhosted.org/packages/31/ad/e5693e1974a28869e7cd244302911955c1cebc0161eb32dfa2b25b6e96f0/protobuf-6.33.4-cp310-abi3-win_amd64.whl", hash = "sha256:8f11ffae31ec67fc2554c2ef891dcb561dae9a2a3ed941f9e134c2db06657dbc", size = 436962, upload-time = "2026-01-12T18:33:31.345Z" }, - { url = "https://files.pythonhosted.org/packages/66/15/6ee23553b6bfd82670207ead921f4d8ef14c107e5e11443b04caeb5ab5ec/protobuf-6.33.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2fe67f6c014c84f655ee06f6f66213f9254b3a8b6bda6cda0ccd4232c73c06f0", size = 427612, upload-time = "2026-01-12T18:33:32.646Z" }, - { url = "https://files.pythonhosted.org/packages/2b/48/d301907ce6d0db75f959ca74f44b475a9caa8fcba102d098d3c3dd0f2d3f/protobuf-6.33.4-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:757c978f82e74d75cba88eddec479df9b99a42b31193313b75e492c06a51764e", size = 324484, upload-time = "2026-01-12T18:33:33.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/1c/e53078d3f7fe710572ab2dcffd993e1e3b438ae71cfc031b71bae44fcb2d/protobuf-6.33.4-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c7c64f259c618f0bef7bee042075e390debbf9682334be2b67408ec7c1c09ee6", size = 339256, upload-time = "2026-01-12T18:33:35.231Z" }, - { url = "https://files.pythonhosted.org/packages/e8/8e/971c0edd084914f7ee7c23aa70ba89e8903918adca179319ee94403701d5/protobuf-6.33.4-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:3df850c2f8db9934de4cf8f9152f8dc2558f49f298f37f90c517e8e5c84c30e9", size = 323311, upload-time = "2026-01-12T18:33:36.305Z" }, - { url = "https://files.pythonhosted.org/packages/75/b1/1dc83c2c661b4c62d56cc081706ee33a4fc2835bd90f965baa2663ef7676/protobuf-6.33.4-py3-none-any.whl", hash = "sha256:1fe3730068fcf2e595816a6c34fe66eeedd37d51d0400b72fabc848811fdc1bc", size = 170532, upload-time = "2026-01-12T18:33:39.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, ] [[package]] name = "psutil" -version = "7.2.1" +version = "7.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" }, - { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" }, - { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" }, - { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" }, - { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" }, - { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" }, - { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] [[package]] @@ -1679,8 +1258,6 @@ version = "0.2.14" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/26/1d/8878c7752febb0f6716a7e1a52cb92ac98871c5aa522cba181878091607c/PyAudio-0.2.14.tar.gz", hash = "sha256:78dfff3879b4994d1f4fc6485646a57755c6ee3c19647a491f790a0895bd2f87", size = 47066, upload-time = "2023-11-07T07:11:48.806Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/f0/b0eab89eafa70a86b7b566a4df2f94c7880a2d483aa8de1c77d335335b5b/PyAudio-0.2.14-cp311-cp311-win32.whl", hash = "sha256:506b32a595f8693811682ab4b127602d404df7dfc453b499c91a80d0f7bad289", size = 144624, upload-time = "2023-11-07T07:11:36.94Z" }, - { url = "https://files.pythonhosted.org/packages/82/d8/f043c854aad450a76e476b0cf9cda1956419e1dacf1062eb9df3c0055abe/PyAudio-0.2.14-cp311-cp311-win_amd64.whl", hash = "sha256:bbeb01d36a2f472ae5ee5e1451cacc42112986abe622f735bb870a5db77cf903", size = 164070, upload-time = "2023-11-07T07:11:38.579Z" }, { url = "https://files.pythonhosted.org/packages/8d/45/8d2b76e8f6db783f9326c1305f3f816d4a12c8eda5edc6a2e1d03c097c3b/PyAudio-0.2.14-cp312-cp312-win32.whl", hash = "sha256:5fce4bcdd2e0e8c063d835dbe2860dac46437506af509353c7f8114d4bacbd5b", size = 144750, upload-time = "2023-11-07T07:11:40.142Z" }, { url = "https://files.pythonhosted.org/packages/b0/6a/d25812e5f79f06285767ec607b39149d02aa3b31d50c2269768f48768930/PyAudio-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:12f2f1ba04e06ff95d80700a78967897a489c05e093e3bffa05a84ed9c0a7fa3", size = 164126, upload-time = "2023-11-07T07:11:41.539Z" }, ] @@ -1707,18 +1284,6 @@ version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/15/86/a57e3c92acd3e1d2fc3dcad683ada191f722e4ac927e1a384b228ec2780a/pycapnp-2.1.0.tar.gz", hash = "sha256:69cc3d861fee1c9b26c73ad2e8a5d51e76ad87e4ff9be33a4fd2fc72f5846aec", size = 689734, upload-time = "2025-09-05T03:50:40.851Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/7c/934750a0ca77431a22e68e11521dcc6b801bea3ff37331d6a519e5ad142e/pycapnp-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:efacc439ec287d9e8a0ebf01a515404eff795659401e65ba6f1819c7b24f4380", size = 1628855, upload-time = "2025-09-05T03:48:32.317Z" }, - { url = "https://files.pythonhosted.org/packages/2e/a2/fd2c10b3f2e5010c747aa946b27fe09f665d65d5dc2afdd31838a3ef2f5d/pycapnp-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3d8af535a8b44dfd71731a191386c6b821b8a4915806948893d18c79f547a8e", size = 1496942, upload-time = "2025-09-05T03:48:34.905Z" }, - { url = "https://files.pythonhosted.org/packages/0b/8a/42bd0e4c094ef534ac6890d34adae580cbbf5b0497fc0a6340bea833a617/pycapnp-2.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:117d1d5ebfc08cc189aca4f771b34fedc1291a3f9417167bd2d9b2a4e607e640", size = 5200170, upload-time = "2025-09-05T03:48:36.502Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/2e92268383135082191c3dea4a9ad184d20b7fb2dda1477fd6ee520fd88e/pycapnp-2.1.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:d881ccc69e381863a88c7b6c7092a6baecb6dfc8c5558d66bc967c7f778fe7bc", size = 5684026, upload-time = "2025-09-05T03:48:38.063Z" }, - { url = "https://files.pythonhosted.org/packages/46/9c/bca1cbd7711c9c0f0f62ca95a49835369a61c4f6527a6900c8982045bf2f/pycapnp-2.1.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:8a4ea330e38ba83f6f03fbdc1f58642eb53e6f6f66734a426fa592dc988d70e9", size = 5709307, upload-time = "2025-09-05T03:48:40.127Z" }, - { url = "https://files.pythonhosted.org/packages/2d/29/cd14676d992c7b166baa7e022b369c15240d408b202410d105b23b25f737/pycapnp-2.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fb2563de4619d359820de9d47b4704e4f7eda193ffc4a56e39cdcd2c8301c336", size = 5386505, upload-time = "2025-09-05T03:48:41.785Z" }, - { url = "https://files.pythonhosted.org/packages/ae/dd/2fc57cebe9be7e4cd3d6aec0b9c8a0db9772c1b17c37cfe4f04c050422cf/pycapnp-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5265d1ae34f9c089fa6983f6c1be404ce480c82b927017290bd703328fa3f5df", size = 6095180, upload-time = "2025-09-05T03:48:43.795Z" }, - { url = "https://files.pythonhosted.org/packages/5a/16/da8c1ada7770a532c859df475533eec5a1b2f5e81a269466a2fe670c5747/pycapnp-2.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b0a56370a868f752375a785bfb7e06b55cbe71605972615d1220c380bc452380", size = 6603414, upload-time = "2025-09-05T03:48:45.457Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e6/a36eacaf2da6a5ac9c6565600e559edf95115ff990aa3379aee8dd7ba4fe/pycapnp-2.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5d7403c25275cf4badf6f9d0c07b1cb94fcdd599df81aba9b621c32b3dcefae9", size = 6621440, upload-time = "2025-09-05T03:48:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/81/54/9150c03638cf4ecdf1664867382d0049146c658d6de30f189817c502df1a/pycapnp-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dea5d0d250fe4851b42cd380a207d773ebae76a990e542a888a5f1442f4c247e", size = 6354219, upload-time = "2025-09-05T03:48:49.336Z" }, - { url = "https://files.pythonhosted.org/packages/66/3e/e49ba2d74456d53b570c8d30a660c3b29ecfea075d5dd663132ff9049f19/pycapnp-2.1.0-cp311-cp311-win32.whl", hash = "sha256:593844c3cd92937eb5e7cd47ea3a62cde2d49a1fc05dba644f513c68f60f1318", size = 1053647, upload-time = "2025-09-05T03:48:51.108Z" }, - { url = "https://files.pythonhosted.org/packages/53/de/2b61908dc6abf25b17fed6b5a3b42a2226ec09467a3944f1d845ac29ef9b/pycapnp-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac13dd30062bb9985ae9ec4feca106af2b4fdac6468a09c7b74ad754f3921a06", size = 1208911, upload-time = "2025-09-05T03:48:53.219Z" }, { url = "https://files.pythonhosted.org/packages/74/0e/66b41ba600e5f2523e900b7cc0d2e8496b397a1f2d6a5b7b323ab83418b7/pycapnp-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d2ec561bc948d11f64f43bf9601bede5d6a603d105ae311bd5583c7130624a4", size = 1619223, upload-time = "2025-09-05T03:48:54.64Z" }, { url = "https://files.pythonhosted.org/packages/40/6e/9bcb30180bd40cb0534124ff7f8ba8746a735018d593f608bf40c97821c0/pycapnp-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132cd97f57f6b6636323ca9b68d389dd90b96e87af38cde31e2b5c5a064f277e", size = 1484321, upload-time = "2025-09-05T03:48:55.85Z" }, { url = "https://files.pythonhosted.org/packages/14/0a/9ee1c9ecaff499e4fd1df2f0335bc20f666ec6ce5cd80f8ab055007f3c9b/pycapnp-2.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:568e79268ba7c02a71fe558a8aec1ae3c0f0e6aff809ff618a46afe4964957d2", size = 5143502, upload-time = "2025-09-05T03:48:57.733Z" }, @@ -1735,11 +1300,11 @@ wheels = [ [[package]] name = "pycparser" -version = "2.23" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] @@ -1763,14 +1328,14 @@ wheels = [ [[package]] name = "pyee" -version = "13.0.0" +version = "13.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, ] [[package]] @@ -1793,16 +1358,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, ] [[package]] @@ -2029,7 +1589,6 @@ version = "12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263, upload-time = "2025-11-14T09:31:35.231Z" }, { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, ] @@ -2044,7 +1603,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/2d/87/8ca40428d05a668fecc638f2f47dba86054dbdc35351d247f039749de955/pyobjc_framework_accessibility-12.1.tar.gz", hash = "sha256:5ff362c3425edc242d49deec11f5f3e26e565cefb6a2872eda59ab7362149772", size = 29800, upload-time = "2025-11-14T10:08:31.949Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/00/182c57584ad8e5946a82dacdc83c9791567e10bffdea1fe92272b3fdec14/pyobjc_framework_accessibility-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5e29dac0ce8327cd5a8b9a5a8bd8aa83e4070018b93699e97ac0c3af09b42a9a", size = 11301, upload-time = "2025-11-14T09:35:28.678Z" }, { url = "https://files.pythonhosted.org/packages/cc/95/9ea0d1c16316b4b5babf4b0515e9a133ac64269d3ec031f15ee9c7c2a8c1/pyobjc_framework_accessibility-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:537691a0b28fedb8385cd093df069a6e5d7e027629671fc47b50210404eca20b", size = 11335, upload-time = "2025-11-14T09:35:30.81Z" }, ] @@ -2071,7 +1629,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/18/28/0404af2a1c6fa8fd266df26fb6196a8f3fb500d6fe3dab94701949247bea/pyobjc_framework_addressbook-12.1.tar.gz", hash = "sha256:c48b740cf981103cef1743d0804a226d86481fcb839bd84b80e9a586187e8000", size = 44359, upload-time = "2025-11-14T10:08:37.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/5a/2ecaa94e5f56c6631f0820ec4209f8075c1b7561fe37495e2d024de1c8df/pyobjc_framework_addressbook-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:681755ada6c95bd4a096bc2b9f9c24661ffe6bff19a96963ee3fad34f3d61d2b", size = 12879, upload-time = "2025-11-14T09:35:45.21Z" }, { url = "https://files.pythonhosted.org/packages/b6/33/da709c69cbb60df9522cd614d5c23c15b649b72e5d62fed1048e75c70e7b/pyobjc_framework_addressbook-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7893dd784322f4674299fb3ca40cb03385e5eddb78defd38f08c0b730813b56c", size = 12894, upload-time = "2025-11-14T09:35:47.498Z" }, ] @@ -2139,7 +1696,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/be/6a/d4e613c8e926a5744fc47a9e9fea08384a510dc4f27d844f7ad7a2d793bd/pyobjc_framework_applicationservices-12.1.tar.gz", hash = "sha256:c06abb74f119bc27aeb41bf1aef8102c0ae1288aec1ac8665ea186a067a8945b", size = 103247, upload-time = "2025-11-14T10:08:52.18Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/86/d07eff705ff909a0ffa96d14fc14026e9fc9dd716233648c53dfd5056b8e/pyobjc_framework_applicationservices-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bdddd492eeac6d14ff2f5bd342aba29e30dffa72a2d358c08444da22129890e2", size = 32784, upload-time = "2025-11-14T09:36:08.755Z" }, { url = "https://files.pythonhosted.org/packages/37/a7/55fa88def5c02732c4b747606ff1cbce6e1f890734bbd00f5596b21eaa02/pyobjc_framework_applicationservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c8f6e2fb3b3e9214ab4864ef04eee18f592b46a986c86ea0113448b310520532", size = 32835, upload-time = "2025-11-14T09:36:11.855Z" }, ] @@ -2179,7 +1735,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/9f/51/f81581e7a3c5cb6c9254c6f1e1ee1d614930493761dec491b5b0d49544b9/pyobjc_framework_audiovideobridging-12.1.tar.gz", hash = "sha256:6230ace6bec1f38e8a727c35d054a7be54e039b3053f98e6dd8d08d6baee2625", size = 38457, upload-time = "2025-11-14T10:09:01.122Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/f8/c614630fa382720bbd42a0ff567378630c36d10f114476d6c70b73f73b49/pyobjc_framework_audiovideobridging-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6bc24a7063b08c7d9f1749a4641430d363b6dba642c04d09b58abcee7a5260cb", size = 11037, upload-time = "2025-11-14T09:36:32.583Z" }, { url = "https://files.pythonhosted.org/packages/f3/8e/a28badfcc6c731696e3d3a8a83927bd844d992f9152f903c2fee355702ca/pyobjc_framework_audiovideobridging-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:010021502649e2cca4e999a7c09358d48c6b0ed83530bbc0b85bba6834340e4b", size = 11052, upload-time = "2025-11-14T09:36:34.475Z" }, ] @@ -2193,7 +1748,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/6c/18/86218de3bf67fc1d810065f353d9df70c740de567ebee8550d476cb23862/pyobjc_framework_authenticationservices-12.1.tar.gz", hash = "sha256:cef71faeae2559f5c0ff9a81c9ceea1c81108e2f4ec7de52a98c269feff7a4b6", size = 58683, upload-time = "2025-11-14T10:09:06.003Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/16/2f19d8a95f0cf8e940f7b7fb506ced805d5522b4118336c8e640c34517ae/pyobjc_framework_authenticationservices-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c15bb81282356f3f062ac79ff4166c93097448edc44b17dcf686e1dac78cc832", size = 20636, upload-time = "2025-11-14T09:36:48.35Z" }, { url = "https://files.pythonhosted.org/packages/f1/1d/e9f296fe1ee9a074ff6c45ce9eb109fc3b45696de000f373265c8e42fd47/pyobjc_framework_authenticationservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6fd5ce10fe5359cbbfe03eb12cab3e01992b32ab65653c579b00ac93cf674985", size = 20738, upload-time = "2025-11-14T09:36:51.094Z" }, ] @@ -2207,7 +1761,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/e4/24/080afe8189c47c4bb3daa191ccfd962400ca31a67c14b0f7c2d002c2e249/pyobjc_framework_automaticassessmentconfiguration-12.1.tar.gz", hash = "sha256:2b732c02d9097682ca16e48f5d3b10056b740bc091e217ee4d5715194c8970b1", size = 21895, upload-time = "2025-11-14T10:09:08.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/c9/4d2785565cc470daa222f93f3d332af97de600aef6bd23507ec07501999d/pyobjc_framework_automaticassessmentconfiguration-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d94a4a3beb77b3b2ab7b610c4b41e28593d15571724a9e6ab196b82acc98dc13", size = 9316, upload-time = "2025-11-14T09:37:05.052Z" }, { url = "https://files.pythonhosted.org/packages/f2/b2/fbec3d649bf275d7a9604e5f56015be02ef8dcf002f4ae4d760436b8e222/pyobjc_framework_automaticassessmentconfiguration-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c2e22ea67d7e6d6a84d968169f83d92b59857a49ab12132de07345adbfea8a62", size = 9332, upload-time = "2025-11-14T09:37:07.083Z" }, ] @@ -2221,7 +1774,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/7e/08/362bf6ac2bba393c46cf56078d4578b692b56857c385e47690637a72f0dd/pyobjc_framework_automator-12.1.tar.gz", hash = "sha256:7491a99347bb30da3a3f744052a03434ee29bee3e2ae520576f7e796740e4ba7", size = 186068, upload-time = "2025-11-14T10:09:20.82Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/99/480e07eef053a2ad2a5cf1e15f71982f21d7f4119daafac338fa0352309c/pyobjc_framework_automator-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f3d96da10d28c5c197193a9d805a13157b1cb694b6c535983f8572f5f8746ea", size = 10016, upload-time = "2025-11-14T09:37:18.621Z" }, { url = "https://files.pythonhosted.org/packages/e3/36/2e8c36ddf20d501f9d344ed694e39021190faffc44b596f3a430bf437174/pyobjc_framework_automator-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4df9aec77f0fbca66cd3534d1b8398fe6f3e3c2748c0fc12fec2546c7f2e3ffd", size = 10034, upload-time = "2025-11-14T09:37:20.293Z" }, ] @@ -2238,7 +1790,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/cd/42/c026ab308edc2ed5582d8b4b93da6b15d1b6557c0086914a4aabedd1f032/pyobjc_framework_avfoundation-12.1.tar.gz", hash = "sha256:eda0bb60be380f9ba2344600c4231dd58a3efafa99fdc65d3673ecfbb83f6fcb", size = 310047, upload-time = "2025-11-14T10:09:40.069Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/5a/4ef36b309138840ff8cd85364f66c29e27023f291004c335a99f6e87e599/pyobjc_framework_avfoundation-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82cc2c2d9ab6cc04feeb4700ff251d00f1fcafff573c63d4e87168ff80adb926", size = 83328, upload-time = "2025-11-14T09:37:40.808Z" }, { url = "https://files.pythonhosted.org/packages/a6/00/ca471e5dd33f040f69320832e45415d00440260bf7f8221a9df4c4662659/pyobjc_framework_avfoundation-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bf634f89265b4d93126153200d885b6de4859ed6b3bc65e69ff75540bc398406", size = 83375, upload-time = "2025-11-14T09:37:47.262Z" }, ] @@ -2253,7 +1804,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/34/a9/e44db1a1f26e2882c140f1d502d508b1f240af9048909dcf1e1a687375b4/pyobjc_framework_avkit-12.1.tar.gz", hash = "sha256:a5c0ddb0cb700f9b09c8afeca2c58952d554139e9bb078236d2355b1fddfb588", size = 28473, upload-time = "2025-11-14T10:09:43.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/68/409ee30f3418b76573c70aa05fa4c38e9b8b1d4864093edcc781d66019c2/pyobjc_framework_avkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:78bd31a8aed48644e5407b444dec8b1e15ff77af765607b52edf88b8f1213ac7", size = 11583, upload-time = "2025-11-14T09:38:17.569Z" }, { url = "https://files.pythonhosted.org/packages/75/34/e77b18f7ed0bd707afd388702e910bdf2d0acee39d1139e8619c916d3eb4/pyobjc_framework_avkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eef2c0a51465de025a4509db05ef18ca2b678bb00ee0a8fbad7fd470edfd58f9", size = 11613, upload-time = "2025-11-14T09:38:19.78Z" }, ] @@ -2267,7 +1817,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/6e/83/15bf6c28ec100dae7f92d37c9e117b3b4ee6b4873db062833e16f1cfd6c4/pyobjc_framework_avrouting-12.1.tar.gz", hash = "sha256:6a6c5e583d14f6501df530a9d0559a32269a821fc8140e3646015f097155cd1c", size = 20031, upload-time = "2025-11-14T10:09:45.701Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/a7/5c5725db9c91b492ffbd4ae3e40025deeb9e60fcc7c8fbd5279b52280b95/pyobjc_framework_avrouting-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a79f05fb66e337cabc19a9d949c8b29a5145c879f42e29ba02b601b7700d1bb", size = 8431, upload-time = "2025-11-14T09:38:33.018Z" }, { url = "https://files.pythonhosted.org/packages/68/54/fa24f666525c1332a11b2de959c9877b0fe08f00f29ecf96964b24246c13/pyobjc_framework_avrouting-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c0fb0d3d260527320377a70c87688ca5e4a208b09fddcae2b4257d7fe9b1e18", size = 8450, upload-time = "2025-11-14T09:38:34.941Z" }, ] @@ -2281,7 +1830,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/34/d1/e917fba82790495152fd3508c5053827658881cf7e9887ba60def5e3f221/pyobjc_framework_backgroundassets-12.1.tar.gz", hash = "sha256:8da34df9ae4519c360c429415477fdaf3fbba5addbc647b3340b8783454eb419", size = 26210, upload-time = "2025-11-14T10:09:48.792Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/49/33c1c3eaf26a7d89dd414e14939d4f02063d66252d0f51c02082350223e0/pyobjc_framework_backgroundassets-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17de7990b5ea8047d447339f9e9e6f54b954ffc06647c830932a1688c4743fea", size = 10763, upload-time = "2025-11-14T09:38:46.671Z" }, { url = "https://files.pythonhosted.org/packages/de/34/bbba61f0e8ecb0fe0da7aa2c9ea15f7cb0dca2fb2914fcdcd77b782b5c11/pyobjc_framework_backgroundassets-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2c11cb98650c1a4bc68eeb4b040541ba96613434c5957e98e9bb363413b23c91", size = 10786, upload-time = "2025-11-14T09:38:48.341Z" }, ] @@ -2298,7 +1846,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/5d/b9/39f9de1730e6f8e73be0e4f0c6087cd9439cbe11645b8052d22e1fb8e69b/pyobjc_framework_browserenginekit-12.1.tar.gz", hash = "sha256:6a1a34a155778ab55ab5f463e885f2a3b4680231264e1fe078e62ddeccce49ed", size = 29120, upload-time = "2025-11-14T10:09:51.582Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/a4/2d576d71b2e4b3e1a9aa9fd62eb73167d90cdc2e07b425bbaba8edd32ff5/pyobjc_framework_browserenginekit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:41229c766fb3e5bba2de5e580776388297303b4d63d3065fef3f67b77ec46c3f", size = 11526, upload-time = "2025-11-14T09:38:58.861Z" }, { url = "https://files.pythonhosted.org/packages/46/e0/8d2cebbfcfd6aacb805ae0ae7ba931f6a39140540b2e1e96719e7be28359/pyobjc_framework_browserenginekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d15766bb841b081447015c9626e2a766febfe651f487893d29c5d72bef976b94", size = 11545, upload-time = "2025-11-14T09:39:00.988Z" }, ] @@ -2338,7 +1885,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/1a/c0/1859d4532d39254df085309aff55b85323576f00a883626325af40da4653/pyobjc_framework_callkit-12.1.tar.gz", hash = "sha256:fd6dc9688b785aab360139d683be56f0844bf68bf5e45d0eb770cb68221083cc", size = 29171, upload-time = "2025-11-14T10:10:01.336Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/f6/aafd14b31e00d59d830f9a8e8e46c4f41a249f0370499d5b017599362cf1/pyobjc_framework_callkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e73beae08e6a32bcced8d5bdb45b52d6a0866dd1485eaaddba6063f17d41fcb0", size = 11273, upload-time = "2025-11-14T09:39:16.837Z" }, { url = "https://files.pythonhosted.org/packages/2e/b7/b3a498b14751b4be6af5272c9be9ded718aa850ebf769b052c7d610a142a/pyobjc_framework_callkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:12adc0ace464a057f8908187698e1d417c6c53619797a69d096f4329bffb1089", size = 11334, upload-time = "2025-11-14T09:39:18.622Z" }, ] @@ -2365,7 +1911,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/d2/6a/f5f0f191956e187db85312cbffcc41bf863670d121b9190b4a35f0d36403/pyobjc_framework_cfnetwork-12.1.tar.gz", hash = "sha256:2d16e820f2d43522c793f55833fda89888139d7a84ca5758548ba1f3a325a88d", size = 44383, upload-time = "2025-11-14T10:10:08.428Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/7e/82aca783499b690163dd19d5ccbba580398970874a3431bfd7c14ceddbb3/pyobjc_framework_cfnetwork-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3bf93c0f3d262f629e72f8dd43384d0930ed8e610b3fc5ff555c0c1a1e05334a", size = 18949, upload-time = "2025-11-14T09:39:32.924Z" }, { url = "https://files.pythonhosted.org/packages/f9/0b/28034e63f3a25b30ede814469c3f57d44268cbced19664c84a8664200f9d/pyobjc_framework_cfnetwork-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:92760da248c757085fc39bce4388a0f6f0b67540e51edf60a92ad60ca907d071", size = 19135, upload-time = "2025-11-14T09:39:36.382Z" }, ] @@ -2395,7 +1940,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ac/ef/67815278023b344a79c7e95f748f647245d6f5305136fc80615254ad447c/pyobjc_framework_classkit-12.1.tar.gz", hash = "sha256:8d1e9dd75c3d14938ff533d88b72bca2d34918e4461f418ea323bfb2498473b4", size = 26298, upload-time = "2025-11-14T10:10:13.406Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/e2/67bd062fbc9761c34b9911ed099ee50ccddc3032779ce420ca40083ee15c/pyobjc_framework_classkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bd90aacc68eff3412204a9040fa81eb18348cbd88ed56d33558349f3e51bff52", size = 8857, upload-time = "2025-11-14T09:39:53.283Z" }, { url = "https://files.pythonhosted.org/packages/87/5e/cf43c647af872499fc8e80cc6ac6e9ad77d9c77861dc2e62bdd9b01473ce/pyobjc_framework_classkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c027a3cd9be5fee3f605589118b8b278297c384a271f224c1a98b224e0c087e6", size = 8877, upload-time = "2025-11-14T09:39:54.979Z" }, ] @@ -2424,7 +1968,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812, upload-time = "2025-11-14T09:40:53.169Z" }, { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, ] @@ -2478,7 +2021,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/4b/a0/ce0542d211d4ea02f5cbcf72ee0a16b66b0d477a4ba5c32e00117703f2f0/pyobjc_framework_contacts-12.1.tar.gz", hash = "sha256:89bca3c5cf31404b714abaa1673577e1aaad6f2ef49d4141c6dbcc0643a789ad", size = 42378, upload-time = "2025-11-14T10:13:14.203Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/f5/5d2c03cf5219f2e35f3f908afa11868e9096aff33b29b41d63f2de3595f2/pyobjc_framework_contacts-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8ab86070895a005239256d207e18209b1a79d35335b6604db160e8375a7165e6", size = 12086, upload-time = "2025-11-14T09:43:03.225Z" }, { url = "https://files.pythonhosted.org/packages/32/c8/2c4638c0d06447886a34070eebb9ba57407d4dd5f0fcb7ab642568272b88/pyobjc_framework_contacts-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2e5ce33b686eb9c0a39351938a756442ea8dea88f6ae2f16bff5494a8569c687", size = 12165, upload-time = "2025-11-14T09:43:05.119Z" }, ] @@ -2493,7 +2035,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/cd/0c/7bb7f898456a81d88d06a1084a42e374519d2e40a668a872b69b11f8c1f9/pyobjc_framework_contactsui-12.1.tar.gz", hash = "sha256:aaeca7c9e0c9c4e224d73636f9a558f9368c2c7422155a41fd4d7a13613a77c1", size = 18769, upload-time = "2025-11-14T10:13:16.301Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/e3/8d330640bf0337289834334c54c599fec2dad38a8a3b736d40bcb5d8db6e/pyobjc_framework_contactsui-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:10e7ce3b105795919605be89ebeecffd656e82dbf1bafa5db6d51d6def2265ee", size = 7871, upload-time = "2025-11-14T09:43:16.973Z" }, { url = "https://files.pythonhosted.org/packages/f3/ab/319aa52dfe6f836f4dc542282c2c13996222d4f5c9ea7ff8f391b12dac83/pyobjc_framework_contactsui-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:057f40d2f6eb1b169a300675ec75cc7a747cddcbcee8ece133e652a7086c5ab5", size = 7888, upload-time = "2025-11-14T09:43:18.502Z" }, ] @@ -2507,7 +2048,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/84/d1/0b884c5564ab952ff5daa949128c64815300556019c1bba0cf2ca752a1a0/pyobjc_framework_coreaudio-12.1.tar.gz", hash = "sha256:a9e72925fcc1795430496ce0bffd4ddaa92c22460a10308a7283ade830089fe1", size = 75077, upload-time = "2025-11-14T10:13:22.345Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/25/491ff549fd9a40be4416793d335bff1911d3d1d1e1635e3b0defbd2cf585/pyobjc_framework_coreaudio-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a452de6b509fa4a20160c0410b72330ac871696cd80237883955a5b3a4de8f2a", size = 35327, upload-time = "2025-11-14T09:43:32.523Z" }, { url = "https://files.pythonhosted.org/packages/a9/48/05b5192122e23140cf583eac99ccc5bf615591d6ff76483ba986c38ee750/pyobjc_framework_coreaudio-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a5ad6309779663f846ab36fe6c49647e470b7e08473c3e48b4f004017bdb68a4", size = 36908, upload-time = "2025-11-14T09:43:36.108Z" }, ] @@ -2522,7 +2062,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/41/1c/5c7e39b9361d4eec99b9115b593edd9825388acd594cb3b4519f8f1ac12c/pyobjc_framework_coreaudiokit-12.1.tar.gz", hash = "sha256:b83624f8de3068ab2ca279f786be0804da5cf904ff9979d96007b69ef4869e1e", size = 20137, upload-time = "2025-11-14T10:13:24.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/53/e4233fbe5b94b124f5612e1edc130a9280c4674a1d1bf42079ea14b816e1/pyobjc_framework_coreaudiokit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e1144c272f8d6429a34a6757700048f4631eb067c4b08d4768ddc28c371a7014", size = 7250, upload-time = "2025-11-14T09:43:53.208Z" }, { url = "https://files.pythonhosted.org/packages/19/d7/f171c04c6496afeaad2ab658b0c810682c8407127edc94d4b3f3b90c2bb1/pyobjc_framework_coreaudiokit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:97d5dd857e73d5b597cfc980972b021314b760e2f5bdde7bbba0334fbf404722", size = 7273, upload-time = "2025-11-14T09:43:55.411Z" }, ] @@ -2536,7 +2075,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/4b/25/d21d6cb3fd249c2c2aa96ee54279f40876a0c93e7161b3304bf21cbd0bfe/pyobjc_framework_corebluetooth-12.1.tar.gz", hash = "sha256:8060c1466d90bbb9100741a1091bb79975d9ba43911c9841599879fc45c2bbe0", size = 33157, upload-time = "2025-11-14T10:13:28.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/7a/26ae106beb97e9c4745065edb3ce3c2bdd91d81f5b52b8224f82ce9d5fb9/pyobjc_framework_corebluetooth-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:37e6456c8a076bd5a2bdd781d0324edd5e7397ef9ac9234a97433b522efb13cf", size = 13189, upload-time = "2025-11-14T09:44:06.229Z" }, { url = "https://files.pythonhosted.org/packages/2a/56/01fef62a479cdd6ff9ee40b6e062a205408ff386ce5ba56d7e14a71fcf73/pyobjc_framework_corebluetooth-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe72c9732ee6c5c793b9543f08c1f5bdd98cd95dfc9d96efd5708ec9d6eeb213", size = 13209, upload-time = "2025-11-14T09:44:08.203Z" }, ] @@ -2550,7 +2088,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/3e/c5/8cd46cd4f1b7cf88bdeed3848f830ea9cdcc4e55cd0287a968a2838033fb/pyobjc_framework_coredata-12.1.tar.gz", hash = "sha256:1e47d3c5e51fdc87a90da62b97cae1bc49931a2bb064db1305827028e1fc0ffa", size = 124348, upload-time = "2025-11-14T10:13:36.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/a8/4c694c85365071baef36013a7460850dcf6ebfea0ba239e52d7293cdcb93/pyobjc_framework_coredata-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c861dc42b786243cbd96d9ea07d74023787d03637ef69a2f75a1191a2f16d9d6", size = 16395, upload-time = "2025-11-14T09:44:21.105Z" }, { url = "https://files.pythonhosted.org/packages/a3/29/fe24dc81e0f154805534923a56fe572c3b296092f086cf5a239fccc2d46a/pyobjc_framework_coredata-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a3ee3581ca23ead0b152257e98622fe0bf7e7948f30a62a25a17cafe28fe015e", size = 16409, upload-time = "2025-11-14T09:44:23.582Z" }, ] @@ -2577,7 +2114,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/cc/79/b75885e0d75397dc2fe1ed9ca80be2b64c18b817f5fb924277cb1bf7b163/pyobjc_framework_corelocation-12.1.tar.gz", hash = "sha256:3674e9353f949d91dde6230ad68f6d5748a7f0424751e08a2c09d06050d66231", size = 53511, upload-time = "2025-11-14T10:13:43.384Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/ac/44b6cb414ce647da8328d0ed39f0a8b6eb54e72189ce9049678ce2cb04c3/pyobjc_framework_corelocation-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ffc96b9ba504b35fe3e0fcfb0153e68fdfca6fe71663d240829ceab2d7122588", size = 12700, upload-time = "2025-11-14T09:44:38.717Z" }, { url = "https://files.pythonhosted.org/packages/71/57/1b670890fbf650f1a00afe5ee897ea3856a4a1417c2304c633ee2e978ed0/pyobjc_framework_corelocation-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8c35ad29a062fea7d417fd8997a9309660ba7963f2847c004e670efbe6bb5b00", size = 12721, upload-time = "2025-11-14T09:44:41.185Z" }, ] @@ -2591,7 +2127,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/da/7d/5ad600ff7aedfef8ba8f51b11d9aaacdf247b870bd14045d6e6f232e3df9/pyobjc_framework_coremedia-12.1.tar.gz", hash = "sha256:166c66a9c01e7a70103f3ca44c571431d124b9070612ef63a1511a4e6d9d84a7", size = 89566, upload-time = "2025-11-14T10:13:49.788Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/bc/e66de468b3777d8fece69279cf6d2af51d2263e9a1ccad21b90c35c74b1b/pyobjc_framework_coremedia-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ee7b822c9bb674b5b0a70bfb133410acae354e9241b6983f075395f3562f3c46", size = 29503, upload-time = "2025-11-14T09:44:54.716Z" }, { url = "https://files.pythonhosted.org/packages/c8/ae/f773cdc33c34a3f9ce6db829dbf72661b65c28ea9efaec8940364185b977/pyobjc_framework_coremedia-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:161a627f5c8cd30a5ebb935189f740e21e6cd94871a9afd463efdb5d51e255fa", size = 29396, upload-time = "2025-11-14T09:44:57.563Z" }, ] @@ -2605,7 +2140,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/08/8e/23baee53ccd6c011c965cff62eb55638b4088c3df27d2bf05004105d6190/pyobjc_framework_coremediaio-12.1.tar.gz", hash = "sha256:880b313b28f00b27775d630174d09e0b53d1cdbadb74216618c9dd5b3eb6806a", size = 51100, upload-time = "2025-11-14T10:13:54.277Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/6c/88514f8938719f74aa13abb9fd5492499f1834391133809b4e125c3e7150/pyobjc_framework_coremediaio-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3da79c5b9785c5ccc1f5982de61d4d0f1ba29717909eb6720734076ccdc0633c", size = 17218, upload-time = "2025-11-14T09:45:15.294Z" }, { url = "https://files.pythonhosted.org/packages/d4/0c/9425c53c9a8c26e468e065ba12ef076bab20197ff7c82052a6dddd46d42b/pyobjc_framework_coremediaio-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1108f8a278928fbca465f95123ea4a56456bd6571c1dc8b91793e6c61d624517", size = 17277, upload-time = "2025-11-14T09:45:17.457Z" }, ] @@ -2619,7 +2153,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/75/96/2d583060a71a73c8a7e6d92f2a02675621b63c1f489f2639e020fae34792/pyobjc_framework_coremidi-12.1.tar.gz", hash = "sha256:3c6f1fd03997c3b0f20ab8545126b1ce5f0cddcc1587dffacad876c161da8c54", size = 55587, upload-time = "2025-11-14T10:13:58.903Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/d5/49b8720ec86f64e3dc3c804bd7e16fabb2a234a9a8b1b6753332ed343b4e/pyobjc_framework_coremidi-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:af3cdf195e8d5e30d1203889cc4107bebc6eb901aaa81bf3faf15e9ffaca0735", size = 24282, upload-time = "2025-11-14T09:45:32.288Z" }, { url = "https://files.pythonhosted.org/packages/e3/2d/99520f6f1685e4cad816e55cbf6d85f8ce6ea908107950e2d37dc17219d8/pyobjc_framework_coremidi-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e84ffc1de59691c04201b0872e184fe55b5589f3a14876bd14460f3b5f3cd109", size = 24317, upload-time = "2025-11-14T09:45:34.92Z" }, ] @@ -2633,7 +2166,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/30/2d/baa9ea02cbb1c200683cb7273b69b4bee5070e86f2060b77e6a27c2a9d7e/pyobjc_framework_coreml-12.1.tar.gz", hash = "sha256:0d1a4216891a18775c9e0170d908714c18e4f53f9dc79fb0f5263b2aa81609ba", size = 40465, upload-time = "2025-11-14T10:14:02.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/0f/f55369da4a33cfe1db38a3512aac4487602783d3a1d572d2c8c4ccce6abc/pyobjc_framework_coreml-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:16dafcfb123f022e62f47a590a7eccf7d0cb5957a77fd5f062b5ee751cb5a423", size = 11331, upload-time = "2025-11-14T09:45:50.445Z" }, { url = "https://files.pythonhosted.org/packages/bb/39/4defef0deb25c5d7e3b7826d301e71ac5b54ef901b7dac4db1adc00f172d/pyobjc_framework_coreml-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:10dc8e8db53d7631ebc712cad146e3a9a9a443f4e1a037e844149a24c3c42669", size = 11356, upload-time = "2025-11-14T09:45:52.271Z" }, ] @@ -2647,7 +2179,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/2c/eb/abef7d405670cf9c844befc2330a46ee59f6ff7bac6f199bf249561a2ca6/pyobjc_framework_coremotion-12.1.tar.gz", hash = "sha256:8e1b094d34084cc8cf07bedc0630b4ee7f32b0215011f79c9e3cd09d205a27c7", size = 33851, upload-time = "2025-11-14T10:14:05.619Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/fd/0d24796779e4d8187abbce5d06cfd7614496d57a68081c5ff1e978b398f9/pyobjc_framework_coremotion-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed8cb67927985d97b1dd23ab6a4a1b716fc7c409c35349816108781efdcbb5b6", size = 10382, upload-time = "2025-11-14T09:46:03.438Z" }, { url = "https://files.pythonhosted.org/packages/bc/75/89fa4aab818aeca21ac0a60b7ceb89a9e685df0ddd3828d36a6f84a0cff0/pyobjc_framework_coremotion-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a77908ab83c422030f913a2a761d196359ab47f6d1e7c76f21de2c6c05ea2f5f", size = 10406, upload-time = "2025-11-14T09:46:05.076Z" }, ] @@ -2662,7 +2193,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/4c/b3/52338a3ff41713f7d7bccaf63bef4ba4a8f2ce0c7eaff39a3629d022a79a/pyobjc_framework_coreservices-12.1.tar.gz", hash = "sha256:fc6a9f18fc6da64c166fe95f2defeb7ac8a9836b3b03bb6a891d36035260dbaa", size = 366150, upload-time = "2025-11-14T10:14:28.133Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/56/c905deb5ab6f7f758faac3f2cbc6f62fde89f8364837b626801bba0975c3/pyobjc_framework_coreservices-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b6ef07bcf99e941395491f1efcf46e99e5fb83eb6bfa12ae5371135d83f731e1", size = 30196, upload-time = "2025-11-14T09:46:19.356Z" }, { url = "https://files.pythonhosted.org/packages/61/6c/33984caaf497fc5a6f86350d7ca4fac8abeb2bc33203edc96955a21e8c05/pyobjc_framework_coreservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8751dc2edcb7cfa248bf8a274c4d6493e8d53ef28a843827a4fc9a0a8b04b8be", size = 30206, upload-time = "2025-11-14T09:46:22.732Z" }, ] @@ -2676,7 +2206,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/99/d0/88ca73b0cf23847af463334989dd8f98e44f801b811e7e1d8a5627ec20b4/pyobjc_framework_corespotlight-12.1.tar.gz", hash = "sha256:57add47380cd0bbb9793f50a4a4b435a90d4ebd2a33698e058cb353ddfb0d068", size = 38002, upload-time = "2025-11-14T10:14:31.948Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/37/1e7bacb9307a8df52234923e054b7303783e7a48a4637d44ce390b015921/pyobjc_framework_corespotlight-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:404a1e362fe19f0dff477edc1665d8ad90aada928246802da777399f7c06b22e", size = 9976, upload-time = "2025-11-14T09:46:45.221Z" }, { url = "https://files.pythonhosted.org/packages/f6/3b/d3031eddff8029859de6d92b1f741625b1c233748889141a6a5a89b96f0e/pyobjc_framework_corespotlight-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bfcea64ab3250e2886d202b8731be3817b5ac0c8c9f43e77d0d5a0b6602e71a7", size = 9996, upload-time = "2025-11-14T09:46:47.157Z" }, ] @@ -2691,7 +2220,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/29/da/682c9c92a39f713bd3c56e7375fa8f1b10ad558ecb075258ab6f1cdd4a6d/pyobjc_framework_coretext-12.1.tar.gz", hash = "sha256:e0adb717738fae395dc645c9e8a10bb5f6a4277e73cba8fa2a57f3b518e71da5", size = 90124, upload-time = "2025-11-14T10:14:38.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/81/7b8efc41e743adfa2d74b92dec263c91bcebfb188d2a8f5eea1886a195ff/pyobjc_framework_coretext-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f6742ba5b0bb7629c345e99eff928fbfd9e9d3d667421ac1a2a43bdb7ba9833", size = 29990, upload-time = "2025-11-14T09:47:01.206Z" }, { url = "https://files.pythonhosted.org/packages/cd/0f/ddf45bf0e3ba4fbdc7772de4728fd97ffc34a0b5a15e1ab1115b202fe4ae/pyobjc_framework_coretext-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d246fa654bdbf43bae3969887d58f0b336c29b795ad55a54eb76397d0e62b93c", size = 30108, upload-time = "2025-11-14T09:47:04.228Z" }, ] @@ -2705,7 +2233,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/88/71/739a5d023566b506b3fd3d2412983faa95a8c16226c0dcd0f67a9294a342/pyobjc_framework_corewlan-12.1.tar.gz", hash = "sha256:a9d82ec71ef61f37e1d611caf51a4203f3dbd8caf827e98128a1afaa0fd2feb5", size = 32417, upload-time = "2025-11-14T10:14:41.921Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/74/4d8a52b930a276f6f9b4f3b1e07cd518cb6d923cb512e39c935e3adb0b86/pyobjc_framework_corewlan-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3e3f2614eb37dfd6860d6a0683877c2f3b909758ef78b68e5f6b7ea9c858cc51", size = 9931, upload-time = "2025-11-14T09:47:20.849Z" }, { url = "https://files.pythonhosted.org/packages/4e/31/3e9cf2c0ac3c979062958eae7a275b602515c9c76fd30680e1ee0fea82ae/pyobjc_framework_corewlan-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5cba04c0550fc777767cd3a5471e4ed837406ab182d7d5c273bc5ce6ea237bfe", size = 9958, upload-time = "2025-11-14T09:47:22.474Z" }, ] @@ -2719,7 +2246,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/6b/7c/d03ff4f74054578577296f33bc669fce16c7827eb1a553bb372b5aab30ca/pyobjc_framework_cryptotokenkit-12.1.tar.gz", hash = "sha256:c95116b4b7a41bf5b54aff823a4ef6f4d9da4d0441996d6d2c115026a42d82f5", size = 32716, upload-time = "2025-11-14T10:14:45.024Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/90/1623b60d6189db08f642777374fd32287b06932c51dfeb1e9ed5bbf67f35/pyobjc_framework_cryptotokenkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d84b75569054fa0886e3e341c00d7179d5fe287e6d1509630dd698ee60ec5af1", size = 12598, upload-time = "2025-11-14T09:47:33.798Z" }, { url = "https://files.pythonhosted.org/packages/0f/c7/aecba253cf21303b2c9f3ce03fc0e987523609d7839ea8e0a688ae816c96/pyobjc_framework_cryptotokenkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ef51a86c1d0125fabdfad0b3efa51098fb03660d8dad2787d82e8b71c9f189de", size = 12633, upload-time = "2025-11-14T09:47:35.707Z" }, ] @@ -2785,7 +2311,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/c4/87/8bd4544793bfcdf507174abddd02b1f077b48fab0004b3db9a63142ce7e9/pyobjc_framework_discrecording-12.1.tar.gz", hash = "sha256:6defc8ea97fb33b4d43870c673710c04c3dc48be30cdf78ba28191a922094990", size = 55607, upload-time = "2025-11-14T10:14:58.276Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/ce/89df4d53a0a5e3a590d6e735eca4f0ba4d1ccc0e0acfbc14164026a3c502/pyobjc_framework_discrecording-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f7d815f28f781e20de0bf278aaa10b0de7e5ea1189aa17676c0bf5b99e9e0d52", size = 14540, upload-time = "2025-11-14T09:47:55.442Z" }, { url = "https://files.pythonhosted.org/packages/c8/70/14a5aa348a5eba16e8773bb56698575cf114aa55aa303037b7000fc53959/pyobjc_framework_discrecording-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:865f1551e58459da6073360afc8f2cc452472c676ba83dcaa9b0c44e7775e4b5", size = 14566, upload-time = "2025-11-14T09:47:57.503Z" }, ] @@ -2878,7 +2403,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/9a/d4/e9b1f74d29ad9dea3d60468d59b80e14ed3a19f9f7a25afcbc10d29c8a1e/pyobjc_framework_extensionkit-12.1.tar.gz", hash = "sha256:773987353e8aba04223dbba3149253db944abfb090c35318b3a770195b75da6d", size = 18694, upload-time = "2025-11-14T10:15:14.104Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/02/3d1df48f838dc9d64f03bedd29f0fdac6c31945251c9818c3e34083eb731/pyobjc_framework_extensionkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9139c064e1c7f21455411848eb39f092af6085a26cad322aa26309260e7929d9", size = 7919, upload-time = "2025-11-14T09:48:22.14Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/8064dad6114a489e5439cc20d9fb0dd64cfc406d875b4a3c87015b3f6266/pyobjc_framework_extensionkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7e01d705c7ac6d080ae34a81db6d9b81875eabefa63fd6eafbfa30f676dd780b", size = 7932, upload-time = "2025-11-14T09:48:23.653Z" }, ] @@ -2892,7 +2416,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/8e/35/86c097ae2fdf912c61c1276e80f3e090a3fc898c75effdf51d86afec456b/pyobjc_framework_externalaccessory-12.1.tar.gz", hash = "sha256:079f770a115d517a6ab87db1b8a62ca6cdf6c35ae65f45eecc21b491e78776c0", size = 20958, upload-time = "2025-11-14T10:15:16.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/01/2a83b63e82ce58722277a00521c3aeec58ac5abb3086704554e47f8becf3/pyobjc_framework_externalaccessory-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:32208e05c9448c8f41b3efdd35dbea4a8b119af190f7a2db0d580be8a5cf962e", size = 8911, upload-time = "2025-11-14T09:48:35.349Z" }, { url = "https://files.pythonhosted.org/packages/ec/52/984034396089766b6e5ff3be0f93470e721c420fa9d1076398557532234f/pyobjc_framework_externalaccessory-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dedbf7a09375ac19668156c1417bd7829565b164a246b714e225b9cbb6a351ad", size = 8932, upload-time = "2025-11-14T09:48:37.393Z" }, ] @@ -2906,7 +2429,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/cc/9a/724b1fae5709f8860f06a6a2a46de568f9bb8bdb2e2aae45b4e010368f51/pyobjc_framework_fileprovider-12.1.tar.gz", hash = "sha256:45034e0d00ae153c991aa01cb1fd41874650a30093e77ba73401dcce5534c8ad", size = 43071, upload-time = "2025-11-14T10:15:19.989Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/37/2f56167e9f43d3b25a5ed073305ca0cfbfc66bedec7aae9e1f2c9c337265/pyobjc_framework_fileprovider-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d527c417f06d27c4908e51d4e6ccce0adcd80c054f19e709626e55c511dc963", size = 20970, upload-time = "2025-11-14T09:48:50.557Z" }, { url = "https://files.pythonhosted.org/packages/2c/f5/56f0751a2988b2caca89d6800c8f29246828d1a7498bb676ef1ab28000b7/pyobjc_framework_fileprovider-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:89b140ea8369512ddf4164b007cbe35b4d97d1dcb8affa12a7264c0ab8d56e45", size = 21003, upload-time = "2025-11-14T09:48:53.128Z" }, ] @@ -2946,7 +2468,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/43/17/21f45d2bca2efc72b975f2dfeae7a163dbeabb1236c1f188578403fd4f09/pyobjc_framework_fsevents-12.1.tar.gz", hash = "sha256:a22350e2aa789dec59b62da869c1b494a429f8c618854b1383d6473f4c065a02", size = 26487, upload-time = "2025-11-14T10:15:26.796Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/3f/a7fe5656b205ee3a9fd828e342157b91e643ee3e5c0d50b12bd4c737f683/pyobjc_framework_fsevents-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:459cc0aac9850c489d238ba778379d09f073bbc3626248855e78c4bc4d97fe46", size = 13059, upload-time = "2025-11-14T09:49:09.814Z" }, { url = "https://files.pythonhosted.org/packages/2d/e3/2c5eeea390c0b053e2d73b223af3ec87a3e99a8106e8d3ee79942edb0822/pyobjc_framework_fsevents-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a2949358513fd7bc622fb362b5c4af4fc24fc6307320070ca410885e5e13d975", size = 13141, upload-time = "2025-11-14T09:49:11.947Z" }, ] @@ -2960,7 +2481,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/a1/55/d00246d6e6d9756e129e1d94bc131c99eece2daa84b2696f6442b8a22177/pyobjc_framework_fskit-12.1.tar.gz", hash = "sha256:ec54e941cdb0b7d800616c06ca76a93685bd7119b8aa6eb4e7a3ee27658fc7ba", size = 42372, upload-time = "2025-11-14T10:15:30.411Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/1a/5a0b6b8dc18b9dbcb7d1ef7bebdd93f12560097dafa6d7c4b3c15649afba/pyobjc_framework_fskit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:95b9135eea81eeed319dcca32c9db04b38688301586180b86c4585fef6b0e9cd", size = 20228, upload-time = "2025-11-14T09:49:25.324Z" }, { url = "https://files.pythonhosted.org/packages/6d/a9/0c47469fe80fa14bc698bb0a5b772b44283cc3aca0f67e7f70ab45e09b24/pyobjc_framework_fskit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:50972897adea86508cfee33ec4c23aa91dede97e9da1640ea2fe74702b065be1", size = 20250, upload-time = "2025-11-14T09:49:28.065Z" }, ] @@ -2974,7 +2494,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/d2/f8/b5fd86f6b722d4259228922e125b50e0a6975120a1c4d957e990fb84e42c/pyobjc_framework_gamecenter-12.1.tar.gz", hash = "sha256:de4118f14c9cf93eb0316d49da410faded3609ce9cd63425e9ef878cebb7ea72", size = 31473, upload-time = "2025-11-14T10:15:33.38Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/17/6491f9e96664e05ec00af7942a6c2f69217771522c9d1180524273cac7cb/pyobjc_framework_gamecenter-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:30943512f2aa8cb129f8e1abf951bf06922ca20b868e918b26c19202f4ee5cc4", size = 18824, upload-time = "2025-11-14T09:49:42.543Z" }, { url = "https://files.pythonhosted.org/packages/16/ee/b496cc4248c5b901e159d6d9a437da9b86a3105fc3999a66744ba2b2c884/pyobjc_framework_gamecenter-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e8d6d10b868be7c00c2d5a0944cc79315945735dcf17eaa3fec1a7986d26be9b", size = 18868, upload-time = "2025-11-14T09:49:44.767Z" }, ] @@ -2988,7 +2507,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/21/14/353bb1fe448cd833839fd199ab26426c0248088753e63c22fe19dc07530f/pyobjc_framework_gamecontroller-12.1.tar.gz", hash = "sha256:64ed3cc4844b67f1faeb540c7cc8d512c84f70b3a4bafdb33d4663a2b2a2b1d8", size = 54554, upload-time = "2025-11-14T10:15:37.591Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/dc/1d8bd4845a46cb5a5c1f860d85394e64729b2447bbe149bb33301bc99056/pyobjc_framework_gamecontroller-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2633c2703fb30ce068b2f5ce145edbd10fd574d2670b5cdee77a9a126f154fec", size = 20913, upload-time = "2025-11-14T09:49:58.863Z" }, { url = "https://files.pythonhosted.org/packages/06/28/9f03d0ef7c78340441f78b19fb2d2c952af04a240da5ed30c7cf2d0d0f4e/pyobjc_framework_gamecontroller-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:878aa6590c1510e91bfc8710d6c880e7a8f3656a7b7b6f4f3af487a6f677ccd5", size = 20949, upload-time = "2025-11-14T09:50:01.608Z" }, ] @@ -3003,7 +2521,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/52/7b/d625c0937557f7e2e64200fdbeb867d2f6f86b2f148b8d6bfe085e32d872/pyobjc_framework_gamekit-12.1.tar.gz", hash = "sha256:014d032c3484093f1409f8f631ba8a0fd2ff7a3ae23fd9d14235340889854c16", size = 63833, upload-time = "2025-11-14T10:15:42.842Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/47/d3b78cf57bc2d62dc1408aaad226b776d167832063bbaa0c7cc7a9a6fa12/pyobjc_framework_gamekit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb263e90a6af3d7294bc1b1ea5907f8e33bb77d62fb806696f8df7e14806ccad", size = 22463, upload-time = "2025-11-14T09:50:16.444Z" }, { url = "https://files.pythonhosted.org/packages/c4/05/1c49e1030dc9f2812fa8049442158be76c32f271075f4571f94e4389ea86/pyobjc_framework_gamekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2eee796d5781157f2c5684f7ef4c2a7ace9d9b408a26a9e7e92e8adf5a3f63d7", size = 22493, upload-time = "2025-11-14T09:50:19.129Z" }, ] @@ -3018,7 +2535,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/e2/11/c310bbc2526f95cce662cc1f1359bb11e2458eab0689737b4850d0f6acb7/pyobjc_framework_gameplaykit-12.1.tar.gz", hash = "sha256:935ebd806d802888969357946245d35a304c530c86f1ffe584e2cf21f0a608a8", size = 41511, upload-time = "2025-11-14T10:15:46.529Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/84/7a4a2c358770f5ffdb6bdabb74dcefdfa248b17c250a7c0f9d16d3b8d987/pyobjc_framework_gameplaykit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b2fb27f9f48c3279937e938a0456a5231b5c89e53e3199b9d54009a0bbd1228a", size = 13125, upload-time = "2025-11-14T09:50:34.384Z" }, { url = "https://files.pythonhosted.org/packages/35/1f/e5fe404f92ec0f9c8c37b00d6cb3ba96ee396c7f91b0a41a39b64bfc2743/pyobjc_framework_gameplaykit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:309b0d7479f702830c9be92dbe5855ac2557a9d23f05f063caf9d9fdb85ff5f0", size = 13150, upload-time = "2025-11-14T09:50:36.884Z" }, ] @@ -3045,7 +2561,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/af/67/436630d00ba1028ea33cc9df2fc28e081481433e5075600f2ea1ff00f45e/pyobjc_framework_healthkit-12.1.tar.gz", hash = "sha256:29c5e5de54b41080b7a4b0207698ac6f600dcb9149becc9c6b3a69957e200e5c", size = 91802, upload-time = "2025-11-14T10:15:54.661Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/37/b23d3c04ee37cbb94ff92caedc3669cd259be0344fcf6bdf1ff75ff0a078/pyobjc_framework_healthkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e67bce41f8f63c11000394c6ce1dc694655d9ff0458771340d2c782f9eafcc6e", size = 20785, upload-time = "2025-11-14T09:50:52.152Z" }, { url = "https://files.pythonhosted.org/packages/65/87/bb1c438de51c4fa733a99ce4d3301e585f14d7efd94031a97707c0be2b46/pyobjc_framework_healthkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:15b6fc958ff5de42888b18dffdec839cb36d2dd8b82076ed2f21a51db5271109", size = 20799, upload-time = "2025-11-14T09:50:54.531Z" }, ] @@ -3059,7 +2574,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/6d/a1/39347381fc7d3cd5ab942d86af347b25c73f0ddf6f5227d8b4d8f5328016/pyobjc_framework_imagecapturecore-12.1.tar.gz", hash = "sha256:c4776c59f4db57727389d17e1ffd9c567b854b8db52198b3ccc11281711074e5", size = 46397, upload-time = "2025-11-14T10:15:58.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/6b/b34d5c9041e90b8a82d87025a1854b60a8ec2d88d9ef9e715f3a40109ed5/pyobjc_framework_imagecapturecore-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:64d1eb677fe5b658a6b6ed734b7120998ea738ca038ec18c4f9c776e90bd9402", size = 15983, upload-time = "2025-11-14T09:51:09.978Z" }, { url = "https://files.pythonhosted.org/packages/50/13/632957b284dec3743d73fb30dbdf03793b3cf1b4c62e61e6484d870f3879/pyobjc_framework_imagecapturecore-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a2777e17ff71fb5a327a897e48c5c7b5a561723a80f990d26e6ed5a1b8748816", size = 16012, upload-time = "2025-11-14T09:51:12.058Z" }, ] @@ -3073,7 +2587,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/5d/b8/d33dd8b7306029bbbd80525bf833fc547e6a223c494bf69a534487283a28/pyobjc_framework_inputmethodkit-12.1.tar.gz", hash = "sha256:f63b6fe2fa7f1412eae63baea1e120e7865e3b68ccfb7d8b0a4aadb309f2b278", size = 23054, upload-time = "2025-11-14T10:16:01.464Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/04/1315f84dba5704a4976ea0185f877f0f33f28781473a817010cee209a8f0/pyobjc_framework_inputmethodkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4e02f49816799a31d558866492048d69e8086178770b76f4c511295610e02ab", size = 9502, upload-time = "2025-11-14T09:51:24.708Z" }, { url = "https://files.pythonhosted.org/packages/01/c2/59bea66405784b25f5d4e821467ba534a0b92dfc98e07257c971e2a8ed73/pyobjc_framework_inputmethodkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0b7d813d46a060572fc0c14ef832e4fe538ebf64e5cab80ee955191792ce0110", size = 9506, upload-time = "2025-11-14T09:51:26.924Z" }, ] @@ -3114,7 +2627,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/f1/a1/3bab6139e94b97eca098e1562f5d6840e3ff10ea1f7fd704a17111a97d5b/pyobjc_framework_intents-12.1.tar.gz", hash = "sha256:bd688c3ab34a18412f56e459e9dae29e1f4152d3c2048fcacdef5fc49dfb9765", size = 132262, upload-time = "2025-11-14T10:16:16.428Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/25/648db47b9c3879fa50c65ab7cc5fbe0dd400cc97141ac2658ef2e196c0b6/pyobjc_framework_intents-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dc68dc49f1f8d9f8d2ffbc0f57ad25caac35312ddc444899707461e596024fec", size = 32134, upload-time = "2025-11-14T09:51:46.369Z" }, { url = "https://files.pythonhosted.org/packages/7a/90/e9489492ae90b4c1ffd02c1221c0432b8768d475787e7887f79032c2487a/pyobjc_framework_intents-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ea9f3e79bf4baf6c7b0fd2d2797184ed51a372bf7f32974b4424f9bd067ef50", size = 32156, upload-time = "2025-11-14T09:51:49.438Z" }, ] @@ -3128,7 +2640,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/19/cf/f0e385b9cfbf153d68efe8d19e5ae672b59acbbfc1f9b58faaefc5ec8c9e/pyobjc_framework_intentsui-12.1.tar.gz", hash = "sha256:16bdf4b7b91c0d1ec9d5513a1182861f1b5b7af95d4f4218ff7cf03032d57f99", size = 19784, upload-time = "2025-11-14T10:16:18.716Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/cc/7678f901cbf5bca8ccace568ae85ee7baddcd93d78754ac43a3bb5e5a7ac/pyobjc_framework_intentsui-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a877555e313d74ac3b10f7b4e738647eea9f744c00a227d1238935ac3f9d7968", size = 8961, upload-time = "2025-11-14T09:52:05.595Z" }, { url = "https://files.pythonhosted.org/packages/f1/17/06812542a9028f5b2dcce56f52f25633c08b638faacd43bad862aad1b41d/pyobjc_framework_intentsui-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cb894fcc4c9ea613a424dcf6fb48142d51174559b82cfdafac8cb47555c842cf", size = 8983, upload-time = "2025-11-14T09:52:07.667Z" }, ] @@ -3142,7 +2653,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/e4/aa/ca3944bbdfead4201b4ae6b51510942c5a7d8e5e2dc3139a071c74061fdf/pyobjc_framework_iobluetooth-12.1.tar.gz", hash = "sha256:8a434118812f4c01dfc64339d41fe8229516864a59d2803e9094ee4cbe2b7edd", size = 155241, upload-time = "2025-11-14T10:16:28.896Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/ab/ad6b36f574c3d52b5e935b1d57ab0f14f4e4cd328cc922d2b6ba6428c12d/pyobjc_framework_iobluetooth-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:77959f2ecf379aa41eb0848fdb25da7c322f9f4a82429965c87c4bc147137953", size = 40415, upload-time = "2025-11-14T09:52:22.069Z" }, { url = "https://files.pythonhosted.org/packages/0b/b6/933b56afb5e84c3c35c074c9e30d7b701c6038989d4867867bdaa7ab618b/pyobjc_framework_iobluetooth-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:111a6e54be9e9dcf77fa2bf84fdac09fae339aa33087d8647ea7ffbd34765d4c", size = 40439, upload-time = "2025-11-14T09:52:26.071Z" }, ] @@ -3234,7 +2744,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/26/e8/75b6b9b3c88b37723c237e5a7600384ea2d84874548671139db02e76652b/pyobjc_framework_libdispatch-12.1.tar.gz", hash = "sha256:4035535b4fae1b5e976f3e0e38b6e3442ffea1b8aa178d0ca89faa9b8ecdea41", size = 38277, upload-time = "2025-11-14T10:16:46.235Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/75/c4aeab6ce7268373d4ceabbc5c406c4bbf557038649784384910932985f8/pyobjc_framework_libdispatch-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:954cc2d817b71383bd267cc5cd27d83536c5f879539122353ca59f1c945ac706", size = 20463, upload-time = "2025-11-14T09:52:55.703Z" }, { url = "https://files.pythonhosted.org/packages/83/6f/96e15c7b2f7b51fc53252216cd0bed0c3541bc0f0aeb32756fefd31bed7d/pyobjc_framework_libdispatch-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e9570d7a9a3136f54b0b834683bf3f206acd5df0e421c30f8fd4f8b9b556789", size = 15650, upload-time = "2025-11-14T09:52:59.284Z" }, ] @@ -3248,7 +2757,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/16/e4/364db7dc26f235e3d7eaab2f92057f460b39800bffdec3128f113388ac9f/pyobjc_framework_libxpc-12.1.tar.gz", hash = "sha256:e46363a735f3ecc9a2f91637750623f90ee74f9938a4e7c833e01233174af44d", size = 35186, upload-time = "2025-11-14T10:16:49.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/c9/701630d025407497b7af50a795ddb6202c184da7f12b46aa683dae3d3552/pyobjc_framework_libxpc-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8d7201db995e5dcd38775fd103641d8fb69b8577d8e6a405c5562e6c0bb72fd1", size = 19620, upload-time = "2025-11-14T09:53:12.529Z" }, { url = "https://files.pythonhosted.org/packages/82/7f/fdec72430f90921b154517a6f9bbeefa7bacfb16b91320742eb16a5955c5/pyobjc_framework_libxpc-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ba93e91e9ca79603dd265382e9f80e9bd32309cd09c8ac3e6489fc5b233676c8", size = 19730, upload-time = "2025-11-14T09:53:17.113Z" }, ] @@ -3277,7 +2785,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/8d/0e/7e5d9a58bb3d5b79a75d925557ef68084171526191b1c0929a887a553d4f/pyobjc_framework_localauthentication-12.1.tar.gz", hash = "sha256:2284f587d8e1206166e4495b33f420c1de486c36c28c4921d09eec858a699d05", size = 29947, upload-time = "2025-11-14T10:16:54.923Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/cb/cf9d13943e13dc868a68844448a7714c16f4ee6ecac384d21aaa5ac43796/pyobjc_framework_localauthentication-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2d7e1b3f987dc387361517525c2c38550dc44dfb3ba42dec3a9fbf35015831a6", size = 10762, upload-time = "2025-11-14T09:53:32.035Z" }, { url = "https://files.pythonhosted.org/packages/05/93/91761ad4e5fa1c3ec25819865d1ccfbee033987147087bff4fcce67a4dc4/pyobjc_framework_localauthentication-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3af1acd287d830cc7f912f46cde0dab054952bde0adaf66c8e8524311a68d279", size = 10773, upload-time = "2025-11-14T09:53:34.074Z" }, ] @@ -3320,7 +2827,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/36/bb/2a668203c20e509a648c35e803d79d0c7f7816dacba74eb5ad8acb186790/pyobjc_framework_mapkit-12.1.tar.gz", hash = "sha256:dbc32dc48e821aaa9b4294402c240adbc1c6834e658a07677b7c19b7990533c5", size = 63520, upload-time = "2025-11-14T10:17:04.221Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/8f/411067e5c5cd23b9fe4c5edfb02ed94417b94eefe56562d36e244edc70ff/pyobjc_framework_mapkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e8aa82d4aae81765c05dcd53fd362af615aa04159fc7a1df1d0eac9c252cb7d5", size = 22493, upload-time = "2025-11-14T09:53:50.112Z" }, { url = "https://files.pythonhosted.org/packages/11/00/a3de41cdf3e6cd7a144e38999fe1ea9777ad19e19d863f2da862e7affe7b/pyobjc_framework_mapkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:84ad7766271c114bdc423e4e2ff5433e5fc6771a3338b5f8e4b54d0340775800", size = 22518, upload-time = "2025-11-14T09:53:52.727Z" }, ] @@ -3349,7 +2855,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/d6/aa/1e8015711df1cdb5e4a0aa0ed4721409d39971ae6e1e71915e3ab72423a3/pyobjc_framework_mediaextension-12.1.tar.gz", hash = "sha256:44409d63cc7d74e5724a68e3f9252cb62fd0fd3ccf0ca94c6a33e5c990149953", size = 39425, upload-time = "2025-11-14T10:17:11.486Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/6f/60b63edf5d27acf450e4937b7193c1a2bd6195fee18e15df6a5734dedb71/pyobjc_framework_mediaextension-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9555f937f2508bd2b6264cba088e2c2e516b2f94a6c804aee40e33fd89c2fb78", size = 38957, upload-time = "2025-11-14T09:54:13.22Z" }, { url = "https://files.pythonhosted.org/packages/2b/ed/99038bcf72ec68e452709af10a087c1377c2d595ba4e66d7a2b0775145d2/pyobjc_framework_mediaextension-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:442bc3a759efb5c154cb75d643a5e182297093533fcdd1c24be6f64f68b93371", size = 38973, upload-time = "2025-11-14T09:54:16.701Z" }, ] @@ -3390,7 +2895,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/a3/71/be5879380a161f98212a336b432256f307d1dcbaaaeb8ec988aea2ada2cd/pyobjc_framework_mediatoolbox-12.1.tar.gz", hash = "sha256:385b48746a5f08756ee87afc14037e552954c427ed5745d7ece31a21a7bad5ab", size = 22305, upload-time = "2025-11-14T10:17:22.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/7a/f20ebd3c590b2cc85cde3e608e49309bfccf9312e4aca7b7ea60908d36d7/pyobjc_framework_mediatoolbox-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74de0cb2d5aaa77e81f8b97eab0f39cd2fab5bf6fa7c6fb5546740cbfb1f8c1f", size = 12656, upload-time = "2025-11-14T09:54:39.215Z" }, { url = "https://files.pythonhosted.org/packages/9c/94/d5ee221f2afbc64b2a7074efe25387cd8700e8116518904b28091ea6ad74/pyobjc_framework_mediatoolbox-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d7bcfeeff3fbf7e9e556ecafd8eaed2411df15c52baf134efa7480494e6faf6d", size = 12818, upload-time = "2025-11-14T09:54:41.251Z" }, ] @@ -3404,7 +2908,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/e7/06/a84f7eb8561d5631954b9458cfca04b690b80b5b85ce70642bc89335f52a/pyobjc_framework_metal-12.1.tar.gz", hash = "sha256:bb554877d5ee2bf3f340ad88e8fe1b85baab7b5ec4bd6ae0f4f7604147e3eae7", size = 181847, upload-time = "2025-11-14T10:17:34.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/cf/edbb8b6dd084df3d235b74dbeb1fc5daf4d063ee79d13fa3bc1cb1779177/pyobjc_framework_metal-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:59e10f9b36d2e409f80f42b6175457a07b18a21ca57ff268f4bc519cd30db202", size = 75920, upload-time = "2025-11-14T09:55:01.048Z" }, { url = "https://files.pythonhosted.org/packages/d0/48/9286d06e1b14c11b65d3fea1555edc0061d9ebe11898dff8a14089e3a4c9/pyobjc_framework_metal-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38ab566b5a2979a43e13593d3eb12000a45e574576fe76996a5e1eb75ad7ac78", size = 75841, upload-time = "2025-11-14T09:55:06.801Z" }, ] @@ -3418,7 +2921,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/f1/09/ce5c74565677fde66de3b9d35389066b19e5d1bfef9d9a4ad80f0c858c0c/pyobjc_framework_metalfx-12.1.tar.gz", hash = "sha256:1551b686fb80083a97879ce0331bdb1d4c9b94557570b7ecc35ebf40ff65c90b", size = 29470, upload-time = "2025-11-14T10:17:37.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/e5/5494639c927085bbba1a310e73662e0bda44b90cdff67fa03a4e1c24d4c4/pyobjc_framework_metalfx-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ec3f7ab036eae45e067fbf209676f98075892aa307d73bb9394304960746cd2", size = 15026, upload-time = "2025-11-14T09:55:35.239Z" }, { url = "https://files.pythonhosted.org/packages/2a/0b/508e3af499694f4eec74cc3ab0530e38db76e43a27db9ecb98c50c68f5f9/pyobjc_framework_metalfx-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a4418ae5c2eb77ec00695fa720a547638dc252dfd77ecb6feb88f713f5a948fd", size = 15062, upload-time = "2025-11-14T09:55:37.352Z" }, ] @@ -3433,7 +2935,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/14/15/5091147aae12d4011a788b93971c3376aaaf9bf32aa935a2c9a06a71e18b/pyobjc_framework_metalkit-12.1.tar.gz", hash = "sha256:14cc5c256f0e3471b412a5b3582cb2a0d36d3d57401a8aa09e433252d1c34824", size = 25473, upload-time = "2025-11-14T10:17:39.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/c5/f72cbc3a5e83211cbfa33b60611abcebbe893854d0f2b28ff6f444f97549/pyobjc_framework_metalkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:28636454f222d9b20cb61f6e8dc1ebd48237903feb4d0dbdf9d7904c542475e5", size = 8735, upload-time = "2025-11-14T09:55:50.053Z" }, { url = "https://files.pythonhosted.org/packages/bf/c0/c8b5b060895cd51493afe3f09909b7e34893b1161cf4d93bc8e3cd306129/pyobjc_framework_metalkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1c4869076571d94788fe539fabfdd568a5c8e340936c7726d2551196640bd152", size = 8755, upload-time = "2025-11-14T09:55:51.683Z" }, ] @@ -3447,7 +2948,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/c5/68/58da38e54aa0d8c19f0d3084d8c84e92d54cc8c9254041f07119d86aa073/pyobjc_framework_metalperformanceshaders-12.1.tar.gz", hash = "sha256:b198e755b95a1de1525e63c3b14327ae93ef1d88359e6be1ce554a3493755b50", size = 137301, upload-time = "2025-11-14T10:17:49.554Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/0f/6dc06a08599a3bc211852a5e6dcb4ed65dfbf1066958feb367ba7702798a/pyobjc_framework_metalperformanceshaders-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0159a6f731dc0fd126481a26490683586864e9d47c678900049a8ffe0135f56", size = 32988, upload-time = "2025-11-14T09:56:05.323Z" }, { url = "https://files.pythonhosted.org/packages/62/84/d505496fca9341e0cb11258ace7640cd986fe3e831f8b4749035e9f82109/pyobjc_framework_metalperformanceshaders-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c00e786c352b3ff5d86cf0cf3a830dc9f6fc32a03ae1a7539d20d11324adb2e8", size = 33242, upload-time = "2025-11-14T09:56:09.354Z" }, ] @@ -3474,7 +2974,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ba/13/5576ddfbc0b174810a49171e2dbe610bdafd3b701011c6ecd9b3a461de8a/pyobjc_framework_metrickit-12.1.tar.gz", hash = "sha256:77841daf6b36ba0c19df88545fd910c0516acf279e6b7b4fa0a712a046eaa9f1", size = 27627, upload-time = "2025-11-14T10:17:56.353Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/b0/e57c60af3e9214e05309dca201abb82e10e8cf91952d90d572b641d62027/pyobjc_framework_metrickit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da6650afd9523cf7a9cae177f4bbd1ad45cc122d97784785fa1482847485142c", size = 8102, upload-time = "2025-11-14T09:56:27.194Z" }, { url = "https://files.pythonhosted.org/packages/b7/04/8da5126e47306438c99750f1dfed430d7cc388f6b7f420ae748f3060ab96/pyobjc_framework_metrickit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3ec96e9ec7dc37fbce57dae277f0d89c66ffe1c3fa2feaca1b7125f8b2b29d87", size = 8120, upload-time = "2025-11-14T09:56:28.73Z" }, ] @@ -3502,7 +3001,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/b4/11/32c358111b623b4a0af9e90470b198fffc068b45acac74e1ba711aee7199/pyobjc_framework_modelio-12.1.tar.gz", hash = "sha256:d041d7bca7c2a4526344d3e593347225b7a2e51a499b3aa548895ba516d1bdbb", size = 66482, upload-time = "2025-11-14T10:18:04.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/c0/c67b806f3f2bb6264a4f7778a2aa82c7b0f50dfac40f6a60366ffc5afaf5/pyobjc_framework_modelio-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1c2c99d47a7e4956a75ce19bddbe2d8ada7d7ce9e2f56ff53fc2898367187749", size = 20180, upload-time = "2025-11-14T09:56:41.924Z" }, { url = "https://files.pythonhosted.org/packages/f6/0e/b8331100f0d658ecb3e87e75c108e2ae8ac7c78b521fd5ad0205b60a2584/pyobjc_framework_modelio-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:68d971917c289fdddf69094c74915d2ccb746b42b150e0bdc16d8161e6164022", size = 20193, upload-time = "2025-11-14T09:56:44.296Z" }, ] @@ -3516,7 +3014,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/87/35/0d0bb6881004cb238cfd7bf74f4b2e42601a1accdf27b2189ec61cf3a2dc/pyobjc_framework_multipeerconnectivity-12.1.tar.gz", hash = "sha256:7123f734b7174cacbe92a51a62b4645cc9033f6b462ff945b504b62e1b9e6c1c", size = 22816, upload-time = "2025-11-14T10:18:07.363Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/eb/e3e4ba158167696498f6491f91a8ac7e24f1ebbab5042cd34318e5d2035c/pyobjc_framework_multipeerconnectivity-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7372e505ed050286aeb83d7e158fda65ad379eae12e1526f32da0a260a8b7d06", size = 11981, upload-time = "2025-11-14T09:56:58.858Z" }, { url = "https://files.pythonhosted.org/packages/33/8d/0646ff7db36942829f0e84be18ba44bc5cd96d6a81651f8e7dc0974821c1/pyobjc_framework_multipeerconnectivity-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1c3bd254a16debed321debf4858f9c9b7d41572ddf1058a4bacf6a5bcfedeeff", size = 12001, upload-time = "2025-11-14T09:57:01.027Z" }, ] @@ -3556,7 +3053,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/38/13/a71270a1b0a9ec979e68b8ec84b0f960e908b17b51cb3cac246a74d52b6b/pyobjc_framework_network-12.1.tar.gz", hash = "sha256:dbf736ff84d1caa41224e86ff84d34b4e9eb6918ae4e373a44d3cb597648a16a", size = 56990, upload-time = "2025-11-14T10:18:16.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/7c/4f9fc6b94be3e949b7579128cbb9171943e27d1d7841db12d66b76aeadc3/pyobjc_framework_network-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d1ad948b9b977f432bf05363381d7d85a7021246ebf9d50771b35bf8d4548d2b", size = 19593, upload-time = "2025-11-14T09:57:17.027Z" }, { url = "https://files.pythonhosted.org/packages/9d/ef/a53f04f43e93932817f2ea71689dcc8afe3b908d631c21d11ec30c7b2e87/pyobjc_framework_network-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5e53aad64eae2933fe12d49185d66aca62fb817abf8a46f86b01e436ce1b79e4", size = 19613, upload-time = "2025-11-14T09:57:19.571Z" }, ] @@ -3570,7 +3066,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/bf/3e/ac51dbb2efa16903e6af01f3c1f5a854c558661a7a5375c3e8767ac668e8/pyobjc_framework_networkextension-12.1.tar.gz", hash = "sha256:36abc339a7f214ab6a05cb2384a9df912f247163710741e118662bd049acfa2e", size = 62796, upload-time = "2025-11-14T10:18:21.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/4e/aa34fc983f001cdb1afbbb4d08b42fd019fc9816caca0bf0b166db1688c1/pyobjc_framework_networkextension-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c3082c29f94ca3a05cd1f3219999ca3af9b6dece1302ccf789f347e612bb9303", size = 14368, upload-time = "2025-11-14T09:57:33.748Z" }, { url = "https://files.pythonhosted.org/packages/f6/14/4934b10ade5ad0518001bfc25260d926816b9c7d08d85ef45e8a61fdef1b/pyobjc_framework_networkextension-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:adc9baacfc532944d67018e381c7645f66a9fa0064939a5a841476d81422cdcc", size = 14376, upload-time = "2025-11-14T09:57:36.132Z" }, ] @@ -3584,7 +3079,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/c6/12/ae0fe82fb1e02365c9fe9531c9de46322f7af09e3659882212c6bf24d75e/pyobjc_framework_notificationcenter-12.1.tar.gz", hash = "sha256:2d09f5ab9dc39770bae4fa0c7cfe961e6c440c8fc465191d403633dccc941094", size = 21282, upload-time = "2025-11-14T10:18:24.51Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/aa/03526fc0cc285c0f8cf31c74ce3a7a464011cc8fa82a35a1637d9878c788/pyobjc_framework_notificationcenter-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84e254f2a56ff5372793dea938a2b2683dd0bc40c5107fede76f9c2c1f6641a2", size = 9871, upload-time = "2025-11-14T09:57:49.208Z" }, { url = "https://files.pythonhosted.org/packages/d8/05/3168637dd425257df5693c2ceafecf92d2e6833c0aaa6594d894a528d797/pyobjc_framework_notificationcenter-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82a735bd63f315f0a56abd206373917b7d09a0ae35fd99f1639a0fac4c525c0a", size = 9895, upload-time = "2025-11-14T09:57:51.151Z" }, ] @@ -3626,7 +3120,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/12/42/805c9b4ac6ad25deb4215989d8fc41533d01e07ffd23f31b65620bade546/pyobjc_framework_oslog-12.1.tar.gz", hash = "sha256:d0ec6f4e3d1689d5e4341bc1130c6f24cb4ad619939f6c14d11a7e80c0ac4553", size = 21193, upload-time = "2025-11-14T10:18:33.645Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/d5/8d37c2e733bd8a9a16546ceca07809d14401a059f8433cdc13579cd6a41a/pyobjc_framework_oslog-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8dd03386331fbb6b39df8941d99071da0bfeda7d10f6434d1daa1c69f0e7bb14", size = 7802, upload-time = "2025-11-14T09:58:05.619Z" }, { url = "https://files.pythonhosted.org/packages/ee/60/0b742347d484068e9d6867cd95dedd1810c790b6aca45f6ef1d0f089f1f5/pyobjc_framework_oslog-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:072a41d36fcf780a070f13ac2569f8bafbb5ae4792fab4136b1a4d602dd9f5b4", size = 7813, upload-time = "2025-11-14T09:58:07.768Z" }, ] @@ -3640,7 +3133,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/6c/d4/2afb59fb0f99eb2f03888850887e536f1ef64b303fd756283679471a5189/pyobjc_framework_passkit-12.1.tar.gz", hash = "sha256:d8c27c352e86a3549bf696504e6b25af5f2134b173d9dd60d66c6d3da53bb078", size = 53835, upload-time = "2025-11-14T10:18:37.906Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/e6/dabd6b99bdadc50aa0306495d8d0afe4b9b3475c2bafdad182721401a724/pyobjc_framework_passkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cb5c8f0fdc46db6b91c51ee1f41a2b81e9a482c96a0c91c096dcb78a012b740a", size = 14087, upload-time = "2025-11-14T09:58:18.991Z" }, { url = "https://files.pythonhosted.org/packages/d8/dc/9cb27e8b7b00649af5e802815ffa8928bd8a619f2984a1bea7dabd28f741/pyobjc_framework_passkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7e95a484ec529dbf1d44f5f7f1406502a77bda733511e117856e3dca9fa29c5c", size = 14102, upload-time = "2025-11-14T09:58:20.903Z" }, ] @@ -3680,7 +3172,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/b8/53/f8a3dc7f711034d2283e289cd966fb7486028ea132a24260290ff32d3525/pyobjc_framework_photos-12.1.tar.gz", hash = "sha256:adb68aaa29e186832d3c36a0b60b0592a834e24c5263e9d78c956b2b77dce563", size = 47034, upload-time = "2025-11-14T10:18:47.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/e0/8824f7cb167934a8aa1c088b7e6f1b5a9342b14694e76eda95fc736282b2/pyobjc_framework_photos-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f28db92602daac9d760067449fc9bf940594536e65ad542aec47d52b56f51959", size = 12319, upload-time = "2025-11-14T09:58:36.324Z" }, { url = "https://files.pythonhosted.org/packages/13/38/e6f25aec46a1a9d0a310795606cc43f9823d41c3e152114b814b597835a8/pyobjc_framework_photos-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eda8a584a851506a1ebbb2ee8de2cb1ed9e3431e6a642ef6a9543e32117d17b9", size = 12358, upload-time = "2025-11-14T09:58:38.131Z" }, ] @@ -3694,7 +3185,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/40/a5/14c538828ed1a420e047388aedc4a2d7d9292030d81bf6b1ced2ec27b6e9/pyobjc_framework_photosui-12.1.tar.gz", hash = "sha256:9141234bb9d17687f1e8b66303158eccdd45132341fbe5e892174910035f029a", size = 29886, upload-time = "2025-11-14T10:18:50.238Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/6c/d678767bbeafa932b91c88bc8bb3a586a1b404b5564b0dc791702eb376c3/pyobjc_framework_photosui-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:02ca941187b2a2dcbbd4964d7b2a05de869653ed8484dc059a51cc70f520cd07", size = 11688, upload-time = "2025-11-14T09:58:51.84Z" }, { url = "https://files.pythonhosted.org/packages/16/a2/b5afca8039b1a659a2a979bb1bdbdddfdf9b1d2724a2cc4633dca2573d5f/pyobjc_framework_photosui-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:713e3bb25feb5ea891e67260c2c0769cab44a7f11b252023bfcf9f8c29dd1206", size = 11714, upload-time = "2025-11-14T09:58:53.674Z" }, ] @@ -3721,7 +3211,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/a9/45/de756b62709add6d0615f86e48291ee2bee40223e7dde7bbe68a952593f0/pyobjc_framework_pushkit-12.1.tar.gz", hash = "sha256:829a2fc8f4780e75fc2a41217290ee0ff92d4ade43c42def4d7e5af436d8ae82", size = 19465, upload-time = "2025-11-14T10:18:57.727Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/b2/d92045e0d4399feda83ee56a9fd685b5c5c175f7ac8423e2cd9b3d52a9da/pyobjc_framework_pushkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:03f41be8b27d06302ea487a6b250aaf811917a0e7d648cd4043fac759d027210", size = 8158, upload-time = "2025-11-14T09:59:09.593Z" }, { url = "https://files.pythonhosted.org/packages/b9/01/74cf1dd0764c590de05dc1e87d168031e424f834721940b7bb02c67fe821/pyobjc_framework_pushkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7bdf472a55ac65154e03f54ae0bcad64c4cf45e9b1acba62f15107f2bc994d69", size = 8177, upload-time = "2025-11-14T09:59:11.155Z" }, ] @@ -3735,7 +3224,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ef/dcd22b743e38b3c430fce4788176c2c5afa8bfb01085b8143b02d1e75201/pyobjc_framework_quartz-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19f99ac49a0b15dd892e155644fe80242d741411a9ed9c119b18b7466048625a", size = 217795, upload-time = "2025-11-14T09:59:46.922Z" }, { url = "https://files.pythonhosted.org/packages/e9/9b/780f057e5962f690f23fdff1083a4cfda5a96d5b4d3bb49505cac4f624f2/pyobjc_framework_quartz-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7730cdce46c7e985535b5a42c31381af4aa6556e5642dc55b5e6597595e57a16", size = 218798, upload-time = "2025-11-14T10:00:01.236Z" }, ] @@ -3763,7 +3251,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/35/f8/b92af879734d91c1726227e7a03b9e68ab8d9d2bb1716d1a5c29254087f2/pyobjc_framework_replaykit-12.1.tar.gz", hash = "sha256:95801fd35c329d7302b2541f2754e6574bf36547ab869fbbf41e408dfa07268a", size = 23312, upload-time = "2025-11-14T10:21:29.18Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/b1/fab264c6a82a78cd050a773c61dec397c5df7e7969eba3c57e17c8964ea3/pyobjc_framework_replaykit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3a2f9da6939d7695fa40de9c560c20948d31b0cc2f892fdd611fc566a6b83606", size = 10090, upload-time = "2025-11-14T10:01:06.321Z" }, { url = "https://files.pythonhosted.org/packages/6b/fc/c68d2111b2655148d88574959d3d8b21d3a003573013301d4d2a7254c1af/pyobjc_framework_replaykit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b0528c2a6188440fdc2017f0924c0a0f15d0a2f6aa295f1d1c2d6b3894c22f1d", size = 10120, upload-time = "2025-11-14T10:01:08.397Z" }, ] @@ -3777,7 +3264,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/3e/4b/8f896bafbdbfa180a5ba1e21a6f5dc63150c09cba69d85f68708e02866ae/pyobjc_framework_safariservices-12.1.tar.gz", hash = "sha256:6a56f71c1e692bca1f48fe7c40e4c5a41e148b4e3c6cfb185fd80a4d4a951897", size = 25165, upload-time = "2025-11-14T10:21:32.041Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/bb/da1059bfad021c417e090058c0a155419b735b4891a7eedc03177b376012/pyobjc_framework_safariservices-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae709cf7a72ac7b95d2f131349f852d5d7a1729a8d760ea3308883f8269a4c37", size = 7281, upload-time = "2025-11-14T10:01:19.294Z" }, { url = "https://files.pythonhosted.org/packages/67/3a/8c525562fd782c88bc44e8c07fc2c073919f98dead08fffd50f280ef1afa/pyobjc_framework_safariservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b475abc82504fc1c0801096a639562d6a6d37370193e8e4a406de9199a7cea13", size = 7281, upload-time = "2025-11-14T10:01:21.238Z" }, ] @@ -3792,7 +3278,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/f4/bf/ad6bf60ceb61614c9c9f5758190971e9b90c45b1c7a244e45db64138b6c2/pyobjc_framework_safetykit-12.1.tar.gz", hash = "sha256:0cd4850659fb9b5632fd8ad21f2de6863e8303ff0d51c5cc9c0034aac5db08d8", size = 20086, upload-time = "2025-11-14T10:21:34.212Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/68/77f17fba082de7c65176e0d74aacbce5c9c9066d6d6edcde5a537c8c140a/pyobjc_framework_safetykit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c63bcd5d571bba149e28c49c8db06073e54e073b08589e94b850b39a43e52b0", size = 8539, upload-time = "2025-11-14T10:01:31.201Z" }, { url = "https://files.pythonhosted.org/packages/b7/0c/08a20fb7516405186c0fe7299530edd4aa22c24f73290198312447f26c8c/pyobjc_framework_safetykit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e4977f7069a23252053d1a42b1a053aefc19b85c960a5214b05daf3c037a6f16", size = 8550, upload-time = "2025-11-14T10:01:32.885Z" }, ] @@ -3807,7 +3292,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/94/8c/1f4005cf0cb68f84dd98b93bbc0974ee7851bb33d976791c85e042dc2278/pyobjc_framework_scenekit-12.1.tar.gz", hash = "sha256:1bd5b866f31fd829f26feac52e807ed942254fd248115c7c742cfad41d949426", size = 101212, upload-time = "2025-11-14T10:21:41.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/7f/eda261013dc41cc70f3157d1a750712dc29b64fc05be84232006b5cd57e5/pyobjc_framework_scenekit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:01bf1336a7a8bdc96fabde8f3506aa7a7d1905e20a5c46030a57daf0ce2cbd16", size = 33542, upload-time = "2025-11-14T10:01:47.613Z" }, { url = "https://files.pythonhosted.org/packages/d2/f1/4986bd96e0ba0f60bff482a6b135b9d6db65d56578d535751f18f88190f0/pyobjc_framework_scenekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:40aea10098893f0b06191f1e79d7b25e12e36a9265549d324238bdb25c7e6df0", size = 33597, upload-time = "2025-11-14T10:01:51.297Z" }, ] @@ -3822,7 +3306,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/2d/7f/73458db1361d2cb408f43821a1e3819318a0f81885f833d78d93bdc698e0/pyobjc_framework_screencapturekit-12.1.tar.gz", hash = "sha256:50992c6128b35ab45d9e336f0993ddd112f58b8c8c8f0892a9cb42d61bd1f4c9", size = 32573, upload-time = "2025-11-14T10:21:44.497Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/92/fe66408f4bd74f6b6da75977d534a7091efe988301d13da4f009bf54ab71/pyobjc_framework_screencapturekit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae412d397eedf189e763defe3497fcb8dffa5e0b54f62390cb33bf9b1cfb864a", size = 11473, upload-time = "2025-11-14T10:02:09.177Z" }, { url = "https://files.pythonhosted.org/packages/05/a8/533acdbf26e0a908ff640d3a445481f3c948682ca887be6711b5fcf82682/pyobjc_framework_screencapturekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:27df138ce2dfa9d4aae5106d4877e9ed694b5a174643c058f1c48678ffc7001a", size = 11504, upload-time = "2025-11-14T10:02:11.36Z" }, ] @@ -3836,7 +3319,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/7d/99/7cfbce880cea61253a44eed594dce66c2b2fbf29e37eaedcd40cffa949e9/pyobjc_framework_screensaver-12.1.tar.gz", hash = "sha256:c4ca111317c5a3883b7eace0a9e7dd72bc6ffaa2ca954bdec918c3ab7c65c96f", size = 22229, upload-time = "2025-11-14T10:21:47.299Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/8d/87ca0fa0a9eda3097a0f4f2eef1544bf1d984697939fbef7cda7495fddb9/pyobjc_framework_screensaver-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5bd10809005fbe0d68fe651f32a393ce059e90da38e74b6b3cd055ed5b23eaa9", size = 8480, upload-time = "2025-11-14T10:02:22.798Z" }, { url = "https://files.pythonhosted.org/packages/5a/a4/2481711f2e9557b90bac74fa8bf821162cf7b65835732ae560fd52e9037e/pyobjc_framework_screensaver-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a3c90c2299eac6d01add81427ae2f90d7724f15d676261e838d7a7750f812322", size = 8422, upload-time = "2025-11-14T10:02:24.49Z" }, ] @@ -3863,7 +3345,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/0c/cb/adc0a09e8c4755c2281bd12803a87f36e0832a8fc853a2d663433dbb72ce/pyobjc_framework_scriptingbridge-12.1.tar.gz", hash = "sha256:0e90f866a7e6a8aeaf723d04c826657dd528c8c1b91e7a605f8bb947c74ad082", size = 20339, upload-time = "2025-11-14T10:21:51.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/de/0943ee8d7f1a7d8467df6e2ea017a6d5041caff2fb0283f37fea4c4ce370/pyobjc_framework_scriptingbridge-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e6e37e69760d6ac9d813decf135d107760d33e1cdf7335016522235607f6f31b", size = 8335, upload-time = "2025-11-14T10:02:36.654Z" }, { url = "https://files.pythonhosted.org/packages/51/46/e0b07d2b3ff9effb8b1179a6cc681a953d3dfbf0eb8b1d6a0e54cef2e922/pyobjc_framework_scriptingbridge-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8083cd68c559c55a3787b2e74fc983c8665e5078571475aaeabf4f34add36b62", size = 8356, upload-time = "2025-11-14T10:02:38.559Z" }, ] @@ -3890,7 +3371,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/80/aa/796e09a3e3d5cee32ebeebb7dcf421b48ea86e28c387924608a05e3f668b/pyobjc_framework_security-12.1.tar.gz", hash = "sha256:7fecb982bd2f7c4354513faf90ba4c53c190b7e88167984c2d0da99741de6da9", size = 168044, upload-time = "2025-11-14T10:22:06.334Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/3d/8d3a39cd292d7c76ab76233498189bc7170fc80f573b415308464f68c7ee/pyobjc_framework_security-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b2d8819f0fb7b619ec7627a0d8c1cac1a57c5143579ce8ac21548165680684b", size = 41287, upload-time = "2025-11-14T10:02:54.491Z" }, { url = "https://files.pythonhosted.org/packages/76/66/5160c0f938fc0515fe8d9af146aac1b093f7ef285ce797fedae161b6c0e8/pyobjc_framework_security-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab42e55f5b782332be5442750fcd9637ee33247d57c7b1d5801bc0e24ee13278", size = 41280, upload-time = "2025-11-14T10:02:58.097Z" }, ] @@ -3919,7 +3399,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/f9/64/bf5b5d82655112a2314422ee649f1e1e73d4381afa87e1651ce7e8444694/pyobjc_framework_securityinterface-12.1.tar.gz", hash = "sha256:deef11ad03be8d9ff77db6e7ac40f6b641ee2d72eaafcf91040537942472e88b", size = 25552, upload-time = "2025-11-14T10:22:12.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/1c/a01fd56765792d1614eb5e8dc0a7d5467564be6a2056b417c9ec7efc648f/pyobjc_framework_securityinterface-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed599be750122376392e95c2407d57bd94644e8320ddef1d67660e16e96b0d06", size = 10719, upload-time = "2025-11-14T10:03:18.353Z" }, { url = "https://files.pythonhosted.org/packages/59/3e/17889a6de03dc813606bb97887dc2c4c2d4e7c8f266bc439548bae756e90/pyobjc_framework_securityinterface-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5cb5e79a73ea17663ebd29e350401162d93e42343da7d96c77efb38ae64ff01f", size = 10783, upload-time = "2025-11-14T10:03:20.202Z" }, ] @@ -3974,7 +3453,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/0f/8b/8ab209a143c11575a857e2111acc5427fb4986b84708b21324cbcbf5591b/pyobjc_framework_sharedwithyou-12.1.tar.gz", hash = "sha256:167d84794a48f408ee51f885210c616fda1ec4bff3dd8617a4b5547f61b05caf", size = 24791, upload-time = "2025-11-14T10:22:21.248Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/69/3ad9b344808c5619adc253b665f8677829dfb978888227e07233d120cfab/pyobjc_framework_sharedwithyou-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:359c03096a6988371ea89921806bb81483ea509c9aa7114f9cd20efd511b3576", size = 8739, upload-time = "2025-11-14T10:03:36.48Z" }, { url = "https://files.pythonhosted.org/packages/ec/ee/e5113ce985a480d13a0fa3d41a242c8068dc09b3c13210557cf5cc6a544a/pyobjc_framework_sharedwithyou-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a99a6ebc6b6de7bc8663b1f07332fab9560b984a57ce344dc5703f25258f258d", size = 8763, upload-time = "2025-11-14T10:03:38.467Z" }, ] @@ -3988,7 +3466,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/55/ef/84059c5774fd5435551ab7ab40b51271cfb9997b0d21f491c6b429fe57a8/pyobjc_framework_sharedwithyoucore-12.1.tar.gz", hash = "sha256:0813149eeb755d718b146ec9365eb4ca3262b6af9ff9ba7db2f7b6f4fd104518", size = 22350, upload-time = "2025-11-14T10:22:23.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/a1/83e58eca8827a1a9975a9c5de7f8c0bdc73b5f53ee79768d1fdbec6747de/pyobjc_framework_sharedwithyoucore-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f4f9f7fed0768ebbbc2d24248365da2cf5f014b8822b2a1fbbce5fa920f410f1", size = 8512, upload-time = "2025-11-14T10:03:49.176Z" }, { url = "https://files.pythonhosted.org/packages/dd/0e/0c2b0591ebc72d437dccca7a1e7164c5f11dde2189d4f4c707a132bab740/pyobjc_framework_sharedwithyoucore-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed928266ae9d577ff73de72a03bebc66a751918eb59ca660a9eca157392f17be", size = 8530, upload-time = "2025-11-14T10:03:50.839Z" }, ] @@ -4002,7 +3479,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ed/2c/8d82c5066cc376de68ad8c1454b7c722c7a62215e5c2f9dac5b33a6c3d42/pyobjc_framework_shazamkit-12.1.tar.gz", hash = "sha256:71db2addd016874639a224ed32b2000b858802b0370c595a283cce27f76883fe", size = 22518, upload-time = "2025-11-14T10:22:25.996Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/12/09d83a8ac51dc11a574449dea48ffa99b3a7c9baf74afeedb487394d110d/pyobjc_framework_shazamkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0c10ba22de524fbedf06270a71bb0a3dbd4a3853b7002ddf54394589c3be6939", size = 8555, upload-time = "2025-11-14T10:04:02.552Z" }, { url = "https://files.pythonhosted.org/packages/04/5e/7d60d8e7b036b20d0e94cd7c4563e7414653344482e85fbc7facffabc95f/pyobjc_framework_shazamkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e184dd0f61a604b1cfcf44418eb95b943e7b8f536058a29e4b81acadd27a9420", size = 8577, upload-time = "2025-11-14T10:04:04.182Z" }, ] @@ -4042,7 +3518,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/8d/3d/194cf19fe7a56c2be5dfc28f42b3b597a62ebb1e1f52a7dd9c55b917ac6c/pyobjc_framework_speech-12.1.tar.gz", hash = "sha256:2a2a546ba6c52d5dd35ddcfee3fd9226a428043d1719597e8701851a6566afdd", size = 25218, upload-time = "2025-11-14T10:22:32.505Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/54/77e12e4c23a98fc49d874f9703c9f8fd0257d64bb0c6ae329b91fc7a99e3/pyobjc_framework_speech-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0301bfae5d0d09b6e69bd4dbabc5631209e291cc40bda223c69ed0c618f8f2dc", size = 9248, upload-time = "2025-11-14T10:04:19.73Z" }, { url = "https://files.pythonhosted.org/packages/f9/1b/224cb98c9c32a6d5e68072f89d26444095be54c6f461efe4fefe9d1330a5/pyobjc_framework_speech-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cae4b88ef9563157a6c9e66b37778fc4022ee44dd1a2a53081c2adbb69698945", size = 9254, upload-time = "2025-11-14T10:04:21.361Z" }, ] @@ -4057,7 +3532,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/b6/78/d683ebe0afb49f46d2d21d38c870646e7cb3c2e83251f264e79d357b1b74/pyobjc_framework_spritekit-12.1.tar.gz", hash = "sha256:a851f4ef5aa65cc9e08008644a528e83cb31021a1c0f17ebfce4de343764d403", size = 64470, upload-time = "2025-11-14T10:22:37.569Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/6a/e8e44fc690d898394093f3a1c5fe90110d1fbcc6e3f486764437c022b0f8/pyobjc_framework_spritekit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26fd12944684713ae1e3cdd229348609c1142e60802624161ca0c3540eec3ffa", size = 17736, upload-time = "2025-11-14T10:04:33.202Z" }, { url = "https://files.pythonhosted.org/packages/3b/38/97c3b6c3437e3e9267fb4e1cd86e0da4eff07e0abe7cd6923644d2dfc878/pyobjc_framework_spritekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1649e57c25145795d04bb6a1ec44c20ef7cf0af7c60a9f6f5bc7998dd269db1e", size = 17802, upload-time = "2025-11-14T10:04:35.346Z" }, ] @@ -4071,7 +3545,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/00/87/8a66a145feb026819775d44975c71c1c64df4e5e9ea20338f01456a61208/pyobjc_framework_storekit-12.1.tar.gz", hash = "sha256:818452e67e937a10b5c8451758274faa44ad5d4329df0fa85735115fb0608da9", size = 34574, upload-time = "2025-11-14T10:22:40.73Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/41/af2afc4d27bde026cfd3b725ee1b082b2838dcaa9880ab719226957bc7cd/pyobjc_framework_storekit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a29f45bcba9dee4cf73dae05ab0f94d06a32fb052e31414d0c23791c1ec7931c", size = 12810, upload-time = "2025-11-14T10:04:48.693Z" }, { url = "https://files.pythonhosted.org/packages/8a/9f/938985e506de0cc3a543e44e1f9990e9e2fb8980b8f3bcfc8f7921d09061/pyobjc_framework_storekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9fe2d65a2b644bb6b4fdd3002292cba153560917de3dd6cf969431fa32d21dd0", size = 12819, upload-time = "2025-11-14T10:04:50.945Z" }, ] @@ -4099,7 +3572,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/21/91/6d03a988831ddb0fb001b13573560e9a5bcccde575b99350f98fe56a2dd4/pyobjc_framework_syncservices-12.1.tar.gz", hash = "sha256:6a213e93d9ce15128810987e4c5de8c73cfab1564ac8d273e6b437a49965e976", size = 31032, upload-time = "2025-11-14T10:22:45.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/9b/25c117f8ffe15aa6cc447da7f5c179627ebafb2b5ec30dfb5e70fede2549/pyobjc_framework_syncservices-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e81a38c2eb7617cb0ecfc4406c1ae2a97c60e95af42e863b2b0f1f6facd9b0da", size = 13380, upload-time = "2025-11-14T10:05:05.814Z" }, { url = "https://files.pythonhosted.org/packages/54/ac/a83cdd120e279ee905e9085afda90992159ed30c6a728b2c56fa2d36b6ea/pyobjc_framework_syncservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cd629bea95692aad2d26196657cde2fbadedae252c7846964228661a600b900", size = 13411, upload-time = "2025-11-14T10:05:07.741Z" }, ] @@ -4113,7 +3585,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/90/7d/50848df8e1c6b5e13967dee9fb91d3391fe1f2399d2d0797d2fc5edb32ba/pyobjc_framework_systemconfiguration-12.1.tar.gz", hash = "sha256:90fe04aa059876a21626931c71eaff742a27c79798a46347fd053d7008ec496e", size = 59158, upload-time = "2025-11-14T10:22:53.056Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/7b/9126a7af1b798998837027390a20b981e0298e51c4c55eed6435967145cb/pyobjc_framework_systemconfiguration-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:796390a80500cc7fde86adc71b11cdc41d09507dd69103d3443fbb60e94fb438", size = 21663, upload-time = "2025-11-14T10:05:21.259Z" }, { url = "https://files.pythonhosted.org/packages/d3/d3/bb935c3d4bae9e6ce4a52638e30eea7039c480dd96bc4f0777c9fabda21b/pyobjc_framework_systemconfiguration-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e5bb9103d39483964431db7125195c59001b7bff2961869cfe157b4c861e52d", size = 21578, upload-time = "2025-11-14T10:05:25.572Z" }, ] @@ -4127,7 +3598,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/12/01/8a706cd3f7dfcb9a5017831f2e6f9e5538298e90052db3bb8163230cbc4f/pyobjc_framework_systemextensions-12.1.tar.gz", hash = "sha256:243e043e2daee4b5c46cd90af5fff46b34596aac25011bab8ba8a37099685eeb", size = 20701, upload-time = "2025-11-14T10:22:58.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/a1/f8df6d59e06bc4b5989a76724e8551935e5b99aff6a21d3592e5ced91f1c/pyobjc_framework_systemextensions-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a4e82160e43c0b1aa17e6d4435e840a655737fbe534e00e37fc1961fbf3bebd", size = 9156, upload-time = "2025-11-14T10:05:39.744Z" }, { url = "https://files.pythonhosted.org/packages/0a/cc/a42883d6ad0ae257a7fa62660b4dd13be15f8fa657922f9a5b6697f26e28/pyobjc_framework_systemextensions-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:01fac4f8d88c0956d9fc714d24811cd070e67200ba811904317d91e849e38233", size = 9166, upload-time = "2025-11-14T10:05:41.479Z" }, ] @@ -4167,7 +3637,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/90/cd/e0253072f221fa89a42fe53f1a2650cc9bf415eb94ae455235bd010ee12e/pyobjc_framework_usernotifications-12.1.tar.gz", hash = "sha256:019ccdf2d400f9a428769df7dba4ea97c02453372bc5f8b75ce7ae54dfe130f9", size = 29749, upload-time = "2025-11-14T10:23:05.364Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/96/aa25bb0727e661a352d1c52e7288e25c12fe77047f988bb45557c17cf2d7/pyobjc_framework_usernotifications-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c62e8d7153d72c4379071e34258aa8b7263fa59212cfffd2f137013667e50381", size = 9632, upload-time = "2025-11-14T10:05:55.166Z" }, { url = "https://files.pythonhosted.org/packages/61/ad/c95053a475246464cba686e16269b0973821601910d1947d088b855a8dac/pyobjc_framework_usernotifications-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:412afb2bf5fe0049f9c4e732e81a8a35d5ebf97c30a5a6abd276259d020c82ac", size = 9644, upload-time = "2025-11-14T10:05:56.801Z" }, ] @@ -4210,7 +3679,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/b3/5f/6995ee40dc0d1a3460ee183f696e5254c0ad14a25b5bc5fd9bd7266c077b/pyobjc_framework_videotoolbox-12.1.tar.gz", hash = "sha256:7adc8670f3b94b086aed6e86c3199b388892edab4f02933c2e2d9b1657561bef", size = 57825, upload-time = "2025-11-14T10:23:13.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/42/53d57b09fd4879988084ec0d9b74c645c9fdd322be594c9601f6cf265dd0/pyobjc_framework_videotoolbox-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a1eb1eb41c0ffdd8dcc6a9b68ab2b5bc50824a85820c8a7802a94a22dfbb4f91", size = 18781, upload-time = "2025-11-14T10:06:11.89Z" }, { url = "https://files.pythonhosted.org/packages/94/a5/91c6c95416f41c412c2079950527cb746c0712ec319c51a6c728c8d6b231/pyobjc_framework_videotoolbox-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eb6ce6837344ee319122066c16ada4beb913e7bfd62188a8d14b1ecbb5a89234", size = 18908, upload-time = "2025-11-14T10:06:14.087Z" }, ] @@ -4224,7 +3692,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/3b/6a/9d110b5521d9b898fad10928818c9f55d66a4af9ac097426c65a9878b095/pyobjc_framework_virtualization-12.1.tar.gz", hash = "sha256:e96afd8e801e92c6863da0921e40a3b68f724804f888bce43791330658abdb0f", size = 40682, upload-time = "2025-11-14T10:23:17.456Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/ee/e18d0d9014c42758d7169144acb2d37eb5ff19bf959db74b20eac706bd8c/pyobjc_framework_virtualization-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a88a307dc96885afc227ceda4067f1af787f024063f4ccf453d59e7afd47cda8", size = 13099, upload-time = "2025-11-14T10:06:27.403Z" }, { url = "https://files.pythonhosted.org/packages/c6/f2/0da47e91f3f8eeda9a8b4bb0d3a0c54a18925009e99b66a8226b9e06ce1e/pyobjc_framework_virtualization-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7d5724b38e64b39ab5ec3b45993afa29fc88b307d99ee2c7a1c0fd770e9b4b21", size = 13131, upload-time = "2025-11-14T10:06:29.337Z" }, ] @@ -4240,7 +3707,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/c2/5a/08bb3e278f870443d226c141af14205ff41c0274da1e053b72b11dfc9fb2/pyobjc_framework_vision-12.1.tar.gz", hash = "sha256:a30959100e85dcede3a786c544e621ad6eb65ff6abf85721f805822b8c5fe9b0", size = 59538, upload-time = "2025-11-14T10:23:21.979Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/37/e30cf4eef2b4c7e20ccadc1249117c77305fbc38b2e5904eb42e3753f63c/pyobjc_framework_vision-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1edbf2fc18ce3b31108f845901a88f2236783ae6bf0bc68438d7ece572dc2a29", size = 21432, upload-time = "2025-11-14T10:06:42.373Z" }, { url = "https://files.pythonhosted.org/packages/3a/5a/23502935b3fc877d7573e743fc3e6c28748f33a45c43851d503bde52cde7/pyobjc_framework_vision-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6b3211d84f3a12aad0cde752cfd43a80d0218960ac9e6b46b141c730e7d655bd", size = 16625, upload-time = "2025-11-14T10:06:44.422Z" }, ] @@ -4254,38 +3720,9 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/14/10/110a50e8e6670765d25190ca7f7bfeecc47ec4a8c018cb928f4f82c56e04/pyobjc_framework_webkit-12.1.tar.gz", hash = "sha256:97a54dd05ab5266bd4f614e41add517ae62cdd5a30328eabb06792474b37d82a", size = 284531, upload-time = "2025-11-14T10:23:40.287Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/37/5082a0bbe12e48d4ffa53b0c0f09c77a4a6ffcfa119e26fa8dd77c08dc1c/pyobjc_framework_webkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3db734877025614eaef4504fadc0fbbe1279f68686a6f106f2e614e89e0d1a9d", size = 49970, upload-time = "2025-11-14T10:07:01.413Z" }, { url = "https://files.pythonhosted.org/packages/db/67/64920c8d201a7fc27962f467c636c4e763b43845baba2e091a50a97a5d52/pyobjc_framework_webkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:af2c7197447638b92aafbe4847c063b6dd5e1ed83b44d3ce7e71e4c9b042ab5a", size = 50084, upload-time = "2025-11-14T10:07:05.868Z" }, ] -[[package]] -name = "pyopencl" -version = "2026.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "platformdirs" }, - { name = "pytools" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d8/81/fd8a2a695916a82e861bcf17b5b8fd9f81e12c9e5931f9ba536678d7b43a/pyopencl-2026.1.2.tar.gz", hash = "sha256:4397dd0b4cbb8b55f3e09bf87114a2465574506b363890b805b860c348b61970", size = 445132, upload-time = "2026-01-16T22:52:24.765Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/88/abf34e31d572c59203774a66cd81c1e3b3d60b911241483675151149c6f1/pyopencl-2026.1.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:8052e8b402b3ed33ee0807d87d4734f66f67dbafbfb3f5a8b81e478e4d417372", size = 437029, upload-time = "2026-01-16T22:51:30.953Z" }, - { url = "https://files.pythonhosted.org/packages/5c/3d/2dd2d8bbf05a190681582b40fc1ee55b210d00ccebcbb416c62b9f9c81a1/pyopencl-2026.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5e03681c3fe22d5185b16a727d96783e3787e0b65e7a29e4afe01ae0cb4e802", size = 429031, upload-time = "2026-01-16T22:51:32.674Z" }, - { url = "https://files.pythonhosted.org/packages/41/16/e554b3bd20be2e858cfb6683ee6549aeebbe5f769e5b95f561f79340ab20/pyopencl-2026.1.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c8c209d517d1421b17d20b80589a2c39e09ea33350f0367314e1caeed3bc741", size = 689596, upload-time = "2026-01-16T22:51:33.913Z" }, - { url = "https://files.pythonhosted.org/packages/22/a8/1df41cf6c7b25b3bfda14aa0183c6a90eaf849528ba27753eaa25fb26e20/pyopencl-2026.1.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e64e2e34bcfad426bd24b71fdb6b02aa5cb02475147742fe07ef93e81866fc7e", size = 736427, upload-time = "2026-01-16T22:51:36.595Z" }, - { url = "https://files.pythonhosted.org/packages/fc/3d/177b6a675691f7b6f708faef33f981e72fbc4bfed2b1dfa94dc70d0e8a25/pyopencl-2026.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65b151c56b936481d6b6050c2b9bc520840e1402be78c282ba5c01921c25477d", size = 1163888, upload-time = "2026-01-16T22:51:37.973Z" }, - { url = "https://files.pythonhosted.org/packages/e9/fa/5905571d9fa48827c0427a3e664c0213dd045940d581b3b739d83df9c0f6/pyopencl-2026.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cc40003446037f391ca0970694efb0627e2870fabb20ee21be75bc445a39d8f4", size = 1228235, upload-time = "2026-01-16T22:51:39.786Z" }, - { url = "https://files.pythonhosted.org/packages/1e/3d/538c675d078b91680d8d82962110d0c9fd42e1584763d515d6e2e82d8c57/pyopencl-2026.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:b6a8e109ade7db60e8b1beb48df8f080941d0cd77fb2c225ad509c80cdef603e", size = 474753, upload-time = "2026-01-16T22:51:41.771Z" }, - { url = "https://files.pythonhosted.org/packages/cd/34/1497070e44d1689ddbd01d24a2265910e84ebc53457a489b9d2b6e1ac675/pyopencl-2026.1.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:7d88e59901bfe1f9296fd89acd9968f008dc7cfee7995f8cd09c3f1a77119aa6", size = 438145, upload-time = "2026-01-16T22:51:43.658Z" }, - { url = "https://files.pythonhosted.org/packages/5b/a3/71d6af8741b52d3bef443518c1ccfda003adcfa9cc1d0df83dac7005d08c/pyopencl-2026.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f96a3bff8a09d2fa924e7c33dafac6ea3ef7ec70e746d6d8e17ce2d959a6836", size = 428820, upload-time = "2026-01-16T22:51:45.326Z" }, - { url = "https://files.pythonhosted.org/packages/db/ea/c8dbabeceac9cad3dbb368e08e0aa208cc6c6251c5134cc25eb15da03639/pyopencl-2026.1.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d4e8e8215ec4fdee4b235b61977cdb1c4f041b487bdcf357be799f45b423d61", size = 685478, upload-time = "2026-01-16T22:51:46.545Z" }, - { url = "https://files.pythonhosted.org/packages/64/c7/5854ef7471dfee195bcef6348a107525ca4d1b73c15240e6444d490f9920/pyopencl-2026.1.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0052a8ccbd282d8ab196705e31f4c3ab344113ea5d5c3ddaeede00cdcab068b", size = 734017, upload-time = "2026-01-16T22:51:48.277Z" }, - { url = "https://files.pythonhosted.org/packages/3d/79/42d4eec282ed299b38d8136d05545113ec8771a1bd6b10bb4ba83ae1236c/pyopencl-2026.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e43da12a376e9283407c2820b24cceeaa129b042ac710947cf8e07b13e294689", size = 1159871, upload-time = "2026-01-16T22:51:49.569Z" }, - { url = "https://files.pythonhosted.org/packages/a0/9a/fdc5d3bed0440d6206109e051008aa0a54ca131d64314bbd42177b8f0763/pyopencl-2026.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1b14b2cf11dec9e0b75cbd14223d1b3c93950fc3e2f7a306b54fa1b17a2cae0f", size = 1225288, upload-time = "2026-01-16T22:51:51.125Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e3/358c19180e0dab5c7dd1fcacc569e6a7ab02a7fddcb9c954f393ceddb2fa/pyopencl-2026.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:d02d7ecabc8d34590dccffe12346689adc5a1ceb07df5acc4ea6c4db8aa28277", size = 474876, upload-time = "2026-01-16T22:51:52.912Z" }, -] - [[package]] name = "pyopenssl" version = "24.2.1" @@ -4300,11 +3737,11 @@ wheels = [ [[package]] name = "pyparsing" -version = "3.3.1" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] @@ -4316,12 +3753,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, ] -[[package]] -name = "pyprof2calltree" -version = "1.4.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/2a/e9a76261183b4b5e059a6625d7aae0bcb0a77622bc767d4497148ce2e218/pyprof2calltree-1.4.5.tar.gz", hash = "sha256:a635672ff31677486350b2be9a823ef92f740e6354a6aeda8fa4a8a3768e8f2f", size = 10080, upload-time = "2020-04-19T10:39:09.819Z" } - [[package]] name = "pyrect" version = "0.2.0" @@ -4332,9 +3763,6 @@ sdist = { url = "https://files.pythonhosted.org/packages/cb/04/2ba023d5f771b645f name = "pyscreeze" version = "1.0.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pillow", marker = "python_full_version < '3.12'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/ee/f0/cb456ac4f1a73723d5b866933b7986f02bacea27516629c00f8e7da94c2d/pyscreeze-1.0.1.tar.gz", hash = "sha256:cf1662710f1b46aa5ff229ee23f367da9e20af4a78e6e365bee973cad0ead4be", size = 27826, upload-time = "2024-08-20T23:03:07.291Z" } [[package]] @@ -4399,30 +3827,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] -[[package]] -name = "pytest-randomly" -version = "4.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/1d/258a4bf1109258c00c35043f40433be5c16647387b6e7cd5582d638c116b/pytest_randomly-4.0.1.tar.gz", hash = "sha256:174e57bb12ac2c26f3578188490bd333f0e80620c3f47340158a86eca0593cd8", size = 14130, upload-time = "2025-09-12T15:23:00.085Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/3e/a4a9227807b56869790aad3e24472a554b585974fe7e551ea350f50897ae/pytest_randomly-4.0.1-py3-none-any.whl", hash = "sha256:e0dfad2fd4f35e07beff1e47c17fbafcf98f9bf4531fd369d9260e2f858bfcb7", size = 8304, upload-time = "2025-09-12T15:22:58.946Z" }, -] - -[[package]] -name = "pytest-repeat" -version = "0.9.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/80/d4/69e9dbb9b8266df0b157c72be32083403c412990af15c7c15f7a3fd1b142/pytest_repeat-0.9.4.tar.gz", hash = "sha256:d92ac14dfaa6ffcfe6917e5d16f0c9bc82380c135b03c2a5f412d2637f224485", size = 6488, upload-time = "2025-04-07T14:59:53.077Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/d4/8b706b81b07b43081bd68a2c0359fe895b74bf664b20aca8005d2bb3be71/pytest_repeat-0.9.4-py3-none-any.whl", hash = "sha256:c1738b4e412a6f3b3b9e0b8b29fcd7a423e50f87381ad9307ef6f5a8601139f3", size = 4180, upload-time = "2025-04-07T14:59:51.492Z" }, -] - [[package]] name = "pytest-subtests" version = "0.15.0" @@ -4487,20 +3891,6 @@ version = "0.15" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ef/c6/2c5999de3bb1533521f1101e8fe56fd9c266732f4d48011c7c69b29d12ae/python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8", size = 132828, upload-time = "2014-05-31T12:28:59.603Z" } -[[package]] -name = "pytools" -version = "2025.2.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "platformdirs" }, - { name = "siphash24" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/7b/f885a57e61ded45b5b10ca60f0b7575c9fb9a282e7513d0e23a33ee647e1/pytools-2025.2.5.tar.gz", hash = "sha256:a7f5350644d46d98ee9c7e67b4b41693308aa0f5e9b188d8f0694b27dc94e3a2", size = 85594, upload-time = "2025-10-07T15:53:30.49Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/84/c42c29ca4bff35baa286df70b0097e0b1c88fd57e8e6bdb09cb161a6f3c1/pytools-2025.2.5-py3-none-any.whl", hash = "sha256:42e93751ec425781e103bbcd769ba35ecbacd43339c2905401608f2fdc30cf19", size = 98811, upload-time = "2025-10-07T15:53:29.089Z" }, -] - [[package]] name = "pytweening" version = "1.2.0" @@ -4512,9 +3902,6 @@ name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { 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" }, @@ -4558,15 +3945,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, @@ -4600,16 +3978,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, - { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, - { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, - { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, - { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, - { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, - { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, @@ -4620,11 +3988,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, - { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, - { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, - { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] [[package]] @@ -4648,13 +4011,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/7c/4b/858958762c075c54058ee3b0771838fd505ca908871e6a0397b01086e526/raylib-5.5.0.4.tar.gz", hash = "sha256:996506e8a533cd7a6a3ef6c44ec11f9d6936698f2c394a991af8022be33079a0", size = 184413, upload-time = "2025-12-11T15:32:12.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/14/98a78b819d7374dab309525ce45cd591d0d62db7f6ed2d5ed32b8f55d62b/raylib-5.5.0.4-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:09717ed32c9ec1c574370e2e2d30e9bc13876f7e2f2dd6e04dc366dae23e0994", size = 1632797, upload-time = "2025-12-11T15:27:15.429Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f4/ec949f45274cf266875b30b67f8cb7243ecced05080cec54bf65ec73a8b2/raylib-5.5.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cef7b0e238eafc80a3be7e3c656a3ddc94cc523790758b7130df1957ba4ad4ad", size = 1550301, upload-time = "2025-12-11T15:27:17.429Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5a/8f60d367147019acef342746f20121b2341ec6596acd5c7941cb36bda02e/raylib-5.5.0.4-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.whl", hash = "sha256:bdaa119b767f380caf6dd4f9d42ab3bf8596d8fb98737d2951b36924a5a83ac0", size = 2036797, upload-time = "2025-12-11T15:27:20.044Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ad/97dd93c389263c61a3057065f0f70db5fdc3c5768fa383a9b3e989ddb6a7/raylib-5.5.0.4-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:6a5cdeeb803d081342961eb1f7c4161af27e951d9ecf2b56d469d5730fcc6213", size = 2188009, upload-time = "2025-12-11T18:50:05.612Z" }, - { url = "https://files.pythonhosted.org/packages/42/6a/55be04012f3459842389689326910204f985cffcb8989a92475221f5660a/raylib-5.5.0.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4067fa8a6ed3eb78a1162fc2d40ce8c26c26c5ee544019d1902accf21ec22add", size = 2187633, upload-time = "2025-12-11T15:27:22.345Z" }, - { url = "https://files.pythonhosted.org/packages/6b/18/b69d9ad9f4064785ad29c73672d40b36c59c3b3efd1dee264cdff4b48bf6/raylib-5.5.0.4-cp311-cp311-win32.whl", hash = "sha256:f01a769bb0797ab4f6e1efc950d5d8aca53548e97da7f527190a1ca5f671c389", size = 1456775, upload-time = "2025-12-11T15:27:26.776Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7a/4025d9ceeee8e3ae4748b0f6c356c5ce97628bd5da8a056b6782c87f7e65/raylib-5.5.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:34771dea34a30fa4657f35b344d5ebf9eb11d9b62b23d9349742db5c5f3992bd", size = 1705555, upload-time = "2025-12-11T15:27:28.888Z" }, { url = "https://files.pythonhosted.org/packages/95/21/9117d7013997a65f6d51c6f56145b2c583eeba8f7c1af71a60776eaae9b9/raylib-5.5.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f64f71e42fed10e8f3629028c9f5700906e0e573b915cfc2244d7a3f3b2ed9", size = 1635486, upload-time = "2025-12-11T15:27:31.05Z" }, { url = "https://files.pythonhosted.org/packages/1c/a3/e55039c8f49856c5a194f2b81f27ca6ba2d5900024f09435587e177bfaf2/raylib-5.5.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:80bfa053e765d47a9f58d59e321a999184b5a5190e369dd015c12fcfd08d6217", size = 1554132, upload-time = "2025-12-11T15:27:33.291Z" }, { url = "https://files.pythonhosted.org/packages/58/1c/86bee75ecaa577214da16b374f8de70b45885452703f622c63e06baa0b8e/raylib-5.5.0.4-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.whl", hash = "sha256:033240c61c1a1fc06fecff747a183671431a4ce63a0c8aafec59217845f86888", size = 2039888, upload-time = "2025-12-11T15:27:36.059Z" }, @@ -4662,9 +4018,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/e9/0123385e369904335985ebd59157f7a10c89c3a706dffcf6dace863a1fa2/raylib-5.5.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:788830bc371ce067c4930ff46a1b6eca0c9cf27bac88f81b035e4b73cc6bf197", size = 2205629, upload-time = "2025-12-11T15:27:39.491Z" }, { url = "https://files.pythonhosted.org/packages/5c/fa/c25087b39d2db2d833a52b4056ae62db74e64b4be677f816e0b368e65453/raylib-5.5.0.4-cp312-cp312-win32.whl", hash = "sha256:e09f395035484337776c90e6c9955c5876b988db7e13168dcadb6ed11974f8ee", size = 1457266, upload-time = "2025-12-11T15:27:43.798Z" }, { url = "https://files.pythonhosted.org/packages/2c/66/a307e61c953ace906ba68ba1174ed8f1e90e68d5fc3e3af9fb7dc46d68d1/raylib-5.5.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:553043a050a31f2ef072f26d3a70373f838a04733f7c5b26a4e9ee3f8caf06ec", size = 1708354, upload-time = "2025-12-11T15:27:45.979Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ba/fee7e6ae0be850f6581d4084ea97825b7895c8866fa8b2df347d408c8293/raylib-5.5.0.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c318357ce721c62a6848b6d84b26574cd77267e5758cfa2dbc01d4deb2a2b0b8", size = 1211520, upload-time = "2025-12-11T15:28:30.266Z" }, - { url = "https://files.pythonhosted.org/packages/80/a0/847066c6d824f535068112ed362d41c499f9a4aca52b82b74d9dfb1bdfc7/raylib-5.5.0.4-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82a0ea2859d04f3b5b441910881ec48789484463856168fa8f35c7165e11d44c", size = 1433828, upload-time = "2025-12-11T15:28:32.204Z" }, - { url = "https://files.pythonhosted.org/packages/40/c6/a2cfb01d63246602ce49111f08d8716e1c7c2994efe4e14d87450176393c/raylib-5.5.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:871e77547cd3f78d98a47bef491821cd25c879b3b3b79f1973d8fb3f8841cdfb", size = 1572456, upload-time = "2025-12-11T15:28:34.333Z" }, ] [[package]] @@ -4702,28 +4055,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.13" +version = "0.15.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, - { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, - { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, - { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, - { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, - { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, - { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, - { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, - { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, + { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, + { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, ] [[package]] @@ -4737,15 +4089,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.49.0" +version = "2.52.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/94/23ac26616a883f492428d9ee9ad6eee391612125326b784dbfc30e1e7bab/sentry_sdk-2.49.0.tar.gz", hash = "sha256:c1878599cde410d481c04ef50ee3aedd4f600e4d0d253f4763041e468b332c30", size = 387228, upload-time = "2026-01-08T09:56:25.642Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/eb/1b497650eb564701f9a7b8a95c51b2abe9347ed2c0b290ba78f027ebe4ea/sentry_sdk-2.52.0.tar.gz", hash = "sha256:fa0bec872cfec0302970b2996825723d67390cdd5f0229fb9efed93bd5384899", size = 410273, upload-time = "2026-02-04T15:03:54.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/43/1c586f9f413765201234541857cb82fda076f4b0f7bad4a0ec248da39cf3/sentry_sdk-2.49.0-py2.py3-none-any.whl", hash = "sha256:6ea78499133874445a20fe9c826c9e960070abeb7ae0cdf930314ab16bb97aa0", size = 415693, upload-time = "2026-01-08T09:56:21.872Z" }, + { url = "https://files.pythonhosted.org/packages/ca/63/2c6daf59d86b1c30600bff679d039f57fd1932af82c43c0bde1cbc55e8d4/sentry_sdk-2.52.0-py2.py3-none-any.whl", hash = "sha256:931c8f86169fc6f2752cb5c4e6480f0d516112e78750c312e081ababecbaf2ed", size = 435547, upload-time = "2026-02-04T15:03:51.567Z" }, ] [[package]] @@ -4754,16 +4106,6 @@ version = "1.3.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/cd/1b7ba5cad635510720ce19d7122154df96a2387d2a74217be552887c93e5/setproctitle-1.3.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a600eeb4145fb0ee6c287cb82a2884bd4ec5bbb076921e287039dcc7b7cc6dd0", size = 18085, upload-time = "2025-09-05T12:49:22.183Z" }, - { url = "https://files.pythonhosted.org/packages/8f/1a/b2da0a620490aae355f9d72072ac13e901a9fec809a6a24fc6493a8f3c35/setproctitle-1.3.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97a090fed480471bb175689859532709e28c085087e344bca45cf318034f70c4", size = 13097, upload-time = "2025-09-05T12:49:23.322Z" }, - { url = "https://files.pythonhosted.org/packages/18/2e/bd03ff02432a181c1787f6fc2a678f53b7dacdd5ded69c318fe1619556e8/setproctitle-1.3.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1607b963e7b53e24ec8a2cb4e0ab3ae591d7c6bf0a160feef0551da63452b37f", size = 32191, upload-time = "2025-09-05T12:49:24.567Z" }, - { url = "https://files.pythonhosted.org/packages/28/78/1e62fc0937a8549f2220445ed2175daacee9b6764c7963b16148119b016d/setproctitle-1.3.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a20fb1a3974e2dab857870cf874b325b8705605cb7e7e8bcbb915bca896f52a9", size = 33203, upload-time = "2025-09-05T12:49:25.871Z" }, - { url = "https://files.pythonhosted.org/packages/a0/3c/65edc65db3fa3df400cf13b05e9d41a3c77517b4839ce873aa6b4043184f/setproctitle-1.3.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8d961bba676e07d77665204f36cffaa260f526e7b32d07ab3df6a2c1dfb44ba", size = 34963, upload-time = "2025-09-05T12:49:27.044Z" }, - { url = "https://files.pythonhosted.org/packages/a1/32/89157e3de997973e306e44152522385f428e16f92f3cf113461489e1e2ee/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:db0fd964fbd3a9f8999b502f65bd2e20883fdb5b1fae3a424e66db9a793ed307", size = 32398, upload-time = "2025-09-05T12:49:28.909Z" }, - { url = "https://files.pythonhosted.org/packages/4a/18/77a765a339ddf046844cb4513353d8e9dcd8183da9cdba6e078713e6b0b2/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:db116850fcf7cca19492030f8d3b4b6e231278e8fe097a043957d22ce1bdf3ee", size = 33657, upload-time = "2025-09-05T12:49:30.323Z" }, - { url = "https://files.pythonhosted.org/packages/6b/63/f0b6205c64d74d2a24a58644a38ec77bdbaa6afc13747e75973bf8904932/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:316664d8b24a5c91ee244460bdaf7a74a707adaa9e14fbe0dc0a53168bb9aba1", size = 31836, upload-time = "2025-09-05T12:49:32.309Z" }, - { url = "https://files.pythonhosted.org/packages/ba/51/e1277f9ba302f1a250bbd3eedbbee747a244b3cc682eb58fb9733968f6d8/setproctitle-1.3.7-cp311-cp311-win32.whl", hash = "sha256:b74774ca471c86c09b9d5037c8451fff06bb82cd320d26ae5a01c758088c0d5d", size = 12556, upload-time = "2025-09-05T12:49:33.529Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7b/822a23f17e9003dfdee92cd72758441ca2a3680388da813a371b716fb07f/setproctitle-1.3.7-cp311-cp311-win_amd64.whl", hash = "sha256:acb9097213a8dd3410ed9f0dc147840e45ca9797785272928d4be3f0e69e3be4", size = 13243, upload-time = "2025-09-05T12:49:34.553Z" }, { url = "https://files.pythonhosted.org/packages/fb/f0/2dc88e842077719d7384d86cc47403e5102810492b33680e7dadcee64cd8/setproctitle-1.3.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2dc99aec591ab6126e636b11035a70991bc1ab7a261da428491a40b84376654e", size = 18049, upload-time = "2025-09-05T12:49:36.241Z" }, { url = "https://files.pythonhosted.org/packages/f0/b4/50940504466689cda65680c9e9a1e518e5750c10490639fa687489ac7013/setproctitle-1.3.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdd8aa571b7aa39840fdbea620e308a19691ff595c3a10231e9ee830339dd798", size = 13079, upload-time = "2025-09-05T12:49:38.088Z" }, { url = "https://files.pythonhosted.org/packages/d0/99/71630546b9395b095f4082be41165d1078204d1696c2d9baade3de3202d0/setproctitle-1.3.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2906b6c7959cdb75f46159bf0acd8cc9906cf1361c9e1ded0d065fe8f9039629", size = 32932, upload-time = "2025-09-05T12:49:39.271Z" }, @@ -4774,18 +4116,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/b7/06145c238c0a6d2c4bc881f8be230bb9f36d2bf51aff7bddcb796d5eed67/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8828b356114f6b308b04afe398ed93803d7fca4a955dd3abe84430e28d33739", size = 32795, upload-time = "2025-09-05T12:49:46.419Z" }, { url = "https://files.pythonhosted.org/packages/ef/dc/ef76a81fac9bf27b84ed23df19c1f67391a753eed6e3c2254ebcb5133f56/setproctitle-1.3.7-cp312-cp312-win32.whl", hash = "sha256:b0304f905efc845829ac2bc791ddebb976db2885f6171f4a3de678d7ee3f7c9f", size = 12552, upload-time = "2025-09-05T12:49:47.635Z" }, { url = "https://files.pythonhosted.org/packages/e2/5b/a9fe517912cd6e28cf43a212b80cb679ff179a91b623138a99796d7d18a0/setproctitle-1.3.7-cp312-cp312-win_amd64.whl", hash = "sha256:9888ceb4faea3116cf02a920ff00bfbc8cc899743e4b4ac914b03625bdc3c300", size = 13247, upload-time = "2025-09-05T12:49:49.16Z" }, - { url = "https://files.pythonhosted.org/packages/c3/5b/5e1c117ac84e3cefcf8d7a7f6b2461795a87e20869da065a5c087149060b/setproctitle-1.3.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1cac6a4b0252b8811d60b6d8d0f157c0fdfed379ac89c25a914e6346cf355a1", size = 12587, upload-time = "2025-09-05T12:51:21.195Z" }, - { url = "https://files.pythonhosted.org/packages/73/02/b9eadc226195dcfa90eed37afe56b5dd6fa2f0e5220ab8b7867b8862b926/setproctitle-1.3.7-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1704c9e041f2b1dc38f5be4552e141e1432fba3dd52c72eeffd5bc2db04dc65", size = 14286, upload-time = "2025-09-05T12:51:22.61Z" }, - { url = "https://files.pythonhosted.org/packages/28/26/1be1d2a53c2a91ec48fa2ff4a409b395f836798adf194d99de9c059419ea/setproctitle-1.3.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b08b61976ffa548bd5349ce54404bf6b2d51bd74d4f1b241ed1b0f25bce09c3a", size = 13282, upload-time = "2025-09-05T12:51:24.094Z" }, ] [[package]] name = "setuptools" -version = "80.9.0" +version = "82.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, ] [[package]] @@ -4797,14 +4136,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" }, - { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" }, - { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" }, - { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" }, - { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" }, - { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" }, - { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" }, - { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" }, { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, @@ -4815,26 +4146,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, ] -[[package]] -name = "siphash24" -version = "1.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/a2/e049b6fccf7a94bd1b2f68b3059a7d6a7aea86a808cac80cb9ae71ab6254/siphash24-1.8.tar.gz", hash = "sha256:aa932f0af4a7335caef772fdaf73a433a32580405c41eb17ff24077944b0aa97", size = 19946, upload-time = "2025-09-02T20:42:04.856Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/23/f53f5bd8866c6ea3abe434c9f208e76ea027210d8b75cd0e0dc849661c7a/siphash24-1.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4662ac616bce4d3c9d6003a0d398e56f8be408fc53a166b79fad08d4f34268e", size = 76930, upload-time = "2025-09-02T20:41:00.869Z" }, - { url = "https://files.pythonhosted.org/packages/0b/25/aebf246904424a06e7ffb7a40cfa9ea9e590ea0fac82e182e0f5d1f1d7ef/siphash24-1.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:53d6bed0951a99c6d2891fa6f8acfd5ca80c3e96c60bcee99f6fa01a04773b1c", size = 74315, upload-time = "2025-09-02T20:41:02.38Z" }, - { url = "https://files.pythonhosted.org/packages/59/3f/7010407c3416ef052d46550d54afb2581fb247018fc6500af8c66669eff2/siphash24-1.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d114c03648630e9e07dac2fe95442404e4607adca91640d274ece1a4fa71123e", size = 99756, upload-time = "2025-09-02T20:41:03.902Z" }, - { url = "https://files.pythonhosted.org/packages/d4/9f/09c734833e69badd7e3faed806b4372bd6564ae0946bd250d5239885914f/siphash24-1.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88c1a55ff82b127c5d3b96927a430d8859e6a98846a5b979833ac790682dd91b", size = 104044, upload-time = "2025-09-02T20:41:05.505Z" }, - { url = "https://files.pythonhosted.org/packages/24/30/56a26d9141a34433da221f732599e2b23d2d70a966c249a9f00feb9a2915/siphash24-1.8-cp311-cp311-win32.whl", hash = "sha256:9430255e6a1313470f52c07c4a4643c451a5b2853f6d4008e4dda05cafb6ce7c", size = 62196, upload-time = "2025-09-02T20:41:07.299Z" }, - { url = "https://files.pythonhosted.org/packages/47/b2/11b0ae63fd374652544e1b12f72ba2cc3fe6c93c1483bd8ff6935b0a8a4b/siphash24-1.8-cp311-cp311-win_amd64.whl", hash = "sha256:1e4b37e4ef0b4496169adce2a58b6c3f230b5852dfa5f7ad0b2d664596409e47", size = 77162, upload-time = "2025-09-02T20:41:08.878Z" }, - { url = "https://files.pythonhosted.org/packages/7f/82/ce3545ce8052ac7ca104b183415a27ec3335e5ed51978fdd7b433f3cfe5b/siphash24-1.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5ed437c6e6cc96196b38728e57cd30b0427df45223475a90e173f5015ef5ba", size = 78136, upload-time = "2025-09-02T20:41:10.083Z" }, - { url = "https://files.pythonhosted.org/packages/15/88/896c3b91bc9deb78c415448b1db67343917f35971a9e23a5967a9d323b8a/siphash24-1.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4ef78abdf811325c7089a35504df339c48c0007d4af428a044431d329721e56", size = 74588, upload-time = "2025-09-02T20:41:11.251Z" }, - { url = "https://files.pythonhosted.org/packages/12/fd/8dad3f5601db485ba862e1c1f91a5d77fb563650856a6708e9acb40ee53c/siphash24-1.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:065eff55c4fefb3a29fd26afb2c072abf7f668ffd53b91d41f92a1c485fcbe5c", size = 98655, upload-time = "2025-09-02T20:41:12.45Z" }, - { url = "https://files.pythonhosted.org/packages/e3/cc/e0c352624c1f2faad270aeb5cce6e173977ef66b9b5e918aa6f32af896bf/siphash24-1.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6fa84ebfd47677262aa0bcb0f5a70f796f5fc5704b287ee1b65a3bd4fb7a5d", size = 103217, upload-time = "2025-09-02T20:41:13.746Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f6/0b1675bea4d40affcae642d9c7337702a4138b93c544230280712403e968/siphash24-1.8-cp312-cp312-win32.whl", hash = "sha256:6582f73615552ca055e51e03cb02a28e570a641a7f500222c86c2d811b5037eb", size = 63114, upload-time = "2025-09-02T20:41:14.972Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/afefef85d72ed8b5cf1aa9283f712e3cd43c9682fabbc809dec54baa8452/siphash24-1.8-cp312-cp312-win_amd64.whl", hash = "sha256:44ea6d794a7cbe184e1e1da2df81c5ebb672ab3867935c3e87c08bb0c2fa4879", size = 76232, upload-time = "2025-09-02T20:41:16.112Z" }, -] - [[package]] name = "six" version = "1.17.0" @@ -4844,15 +4155,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "smbus2" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/36/afafd43770caae69f04e21402552a8f94a072def46a002fab9357f4852ce/smbus2-0.6.0.tar.gz", hash = "sha256:9b5ff1e998e114730f9dfe0c4babbef06c92468cfb61eaa684e30f225661b95b", size = 17403, upload-time = "2025-12-20T09:02:52.017Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/cf/2e1d6805da6f9c9b3a4358076ff2e072d828ba7fed124edc1b729e210c55/smbus2-0.6.0-py2.py3-none-any.whl", hash = "sha256:03d83d2a9a4afc5ddca0698ccabf101cb3de52bc5aefd7b76778ffb27ff654e0", size = 11849, upload-time = "2025-12-20T09:02:51.219Z" }, -] - [[package]] name = "sortedcontainers" version = "2.4.0" @@ -4864,17 +4166,18 @@ wheels = [ [[package]] name = "sounddevice" -version = "0.5.3" +version = "0.5.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/4f/28e734898b870db15b6474453f19813d3c81b91c806d9e6f867bd6e4dd03/sounddevice-0.5.3.tar.gz", hash = "sha256:cbac2b60198fbab84533697e7c4904cc895ec69d5fb3973556c9eb74a4629b2c", size = 53465, upload-time = "2025-10-19T13:23:57.922Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/f9/2592608737553638fca98e21e54bfec40bf577bb98a61b2770c912aab25e/sounddevice-0.5.5.tar.gz", hash = "sha256:22487b65198cb5bf2208755105b524f78ad173e5ab6b445bdab1c989f6698df3", size = 143191, upload-time = "2026-01-23T18:36:43.529Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/e7/9020e9f0f3df00432728f4c4044387468a743e3d9a4f91123d77be10010e/sounddevice-0.5.3-py3-none-any.whl", hash = "sha256:ea7738baa0a9f9fef7390f649e41c9f2c8ada776180e56c2ffd217133c92a806", size = 32670, upload-time = "2025-10-19T13:23:51.779Z" }, - { url = "https://files.pythonhosted.org/packages/2f/39/714118f8413e0e353436914f2b976665161f1be2b6483ac15a8f61484c14/sounddevice-0.5.3-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:278dc4451fff70934a176df048b77d80d7ce1623a6ec9db8b34b806f3112f9c2", size = 108306, upload-time = "2025-10-19T13:23:53.277Z" }, - { url = "https://files.pythonhosted.org/packages/f5/74/52186e3e5c833d00273f7949a9383adff93692c6e02406bf359cb4d3e921/sounddevice-0.5.3-py3-none-win32.whl", hash = "sha256:845d6927bcf14e84be5292a61ab3359cf8e6b9145819ec6f3ac2619ff089a69c", size = 312882, upload-time = "2025-10-19T13:23:54.829Z" }, - { url = "https://files.pythonhosted.org/packages/66/c7/16123d054aef6d445176c9122bfbe73c11087589b2413cab22aff5a7839a/sounddevice-0.5.3-py3-none-win_amd64.whl", hash = "sha256:f55ad20082efc2bdec06928e974fbcae07bc6c405409ae1334cefe7d377eb687", size = 364025, upload-time = "2025-10-19T13:23:56.362Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl", hash = "sha256:30ff99f6c107f49d25ad16a45cacd8d91c25a1bcdd3e81a206b921a3a6405b1f", size = 32807, upload-time = "2026-01-23T18:36:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/56/f9/c037c35f6d0b6bc3bc7bfb314f1d6f1f9a341328ef47cd63fc4f850a7b27/sounddevice-0.5.5-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:05eb9fd6c54c38d67741441c19164c0dae8ce80453af2d8c4ad2e7823d15b722", size = 108557, upload-time = "2026-01-23T18:36:37.41Z" }, + { url = "https://files.pythonhosted.org/packages/88/a1/d19dd9889cd4bce2e233c4fac007cd8daaf5b9fe6e6a5d432cf17be0b807/sounddevice-0.5.5-py3-none-win32.whl", hash = "sha256:1234cc9b4c9df97b6cbe748146ae0ec64dd7d6e44739e8e42eaa5b595313a103", size = 317765, upload-time = "2026-01-23T18:36:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0e/002ed7c4c1c2ab69031f78989d3b789fee3a7fba9e586eb2b81688bf4961/sounddevice-0.5.5-py3-none-win_amd64.whl", hash = "sha256:cfc6b2c49fb7f555591c78cb8ecf48d6a637fd5b6e1db5fec6ed9365d64b3519", size = 365324, upload-time = "2026-01-23T18:36:40.496Z" }, + { url = "https://files.pythonhosted.org/packages/4e/39/a61d4b83a7746b70d23d9173be688c0c6bfc7173772344b7442c2c155497/sounddevice-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3861901ddd8230d2e0e8ae62ac320cdd4c688d81df89da036dcb812f757bb3e6", size = 317115, upload-time = "2026-01-23T18:36:42.235Z" }, ] [[package]] @@ -4895,71 +4198,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] -[[package]] -name = "tabulate" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } -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 = "tqdm" -version = "4.67.1" +version = "4.67.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] [[package]] name = "ty" -version = "0.0.12" +version = "0.0.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/78/ba1a4ad403c748fbba8be63b7e774a90e80b67192f6443d624c64fe4aaab/ty-0.0.12.tar.gz", hash = "sha256:cd01810e106c3b652a01b8f784dd21741de9fdc47bd595d02c122a7d5cefeee7", size = 4981303, upload-time = "2026-01-14T22:30:48.537Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c3/41ae6346443eedb65b96761abfab890a48ce2aa5a8a27af69c5c5d99064d/ty-0.0.17.tar.gz", hash = "sha256:847ed6c120913e280bf9b54d8eaa7a1049708acb8824ad234e71498e8ad09f97", size = 5167209, upload-time = "2026-02-13T13:26:36.835Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/8f/c21314d074dda5fb13d3300fa6733fd0d8ff23ea83a721818740665b6314/ty-0.0.12-py3-none-linux_armv6l.whl", hash = "sha256:eb9da1e2c68bd754e090eab39ed65edf95168d36cbeb43ff2bd9f86b4edd56d1", size = 9614164, upload-time = "2026-01-14T22:30:44.016Z" }, - { url = "https://files.pythonhosted.org/packages/09/28/f8a4d944d13519d70c486e8f96d6fa95647ac2aa94432e97d5cfec1f42f6/ty-0.0.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c181f42aa19b0ed7f1b0c2d559980b1f1d77cc09419f51c8321c7ddf67758853", size = 9542337, upload-time = "2026-01-14T22:30:05.687Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9c/f576e360441de7a8201daa6dc4ebc362853bc5305e059cceeb02ebdd9a48/ty-0.0.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1f829e1eecd39c3e1b032149db7ae6a3284f72fc36b42436e65243a9ed1173db", size = 8909582, upload-time = "2026-01-14T22:30:46.089Z" }, - { url = "https://files.pythonhosted.org/packages/d6/13/0898e494032a5d8af3060733d12929e3e7716db6c75eac63fa125730a3e7/ty-0.0.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45162e7826e1789cf3374627883cdeb0d56b82473a0771923e4572928e90be3", size = 9384932, upload-time = "2026-01-14T22:30:13.769Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1a/b35b6c697008a11d4cedfd34d9672db2f0a0621ec80ece109e13fca4dfef/ty-0.0.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d11fec40b269bec01e751b2337d1c7ffa959a2c2090a950d7e21c2792442cccd", size = 9453140, upload-time = "2026-01-14T22:30:11.131Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1e/71c9edbc79a3c88a0711324458f29c7dbf6c23452c6e760dc25725483064/ty-0.0.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09d99e37e761a4d2651ad9d5a610d11235fbcbf35dc6d4bc04abf54e7cf894f1", size = 9960680, upload-time = "2026-01-14T22:30:33.621Z" }, - { url = "https://files.pythonhosted.org/packages/0e/75/39375129f62dd22f6ad5a99cd2a42fd27d8b91b235ce2db86875cdad397d/ty-0.0.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d9ca0cdb17bd37397da7b16a7cd23423fc65c3f9691e453ad46c723d121225a1", size = 10904518, upload-time = "2026-01-14T22:30:08.464Z" }, - { url = "https://files.pythonhosted.org/packages/32/5e/26c6d88fafa11a9d31ca9f4d12989f57782ec61e7291d4802d685b5be118/ty-0.0.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcf2757b905e7eddb7e456140066335b18eb68b634a9f72d6f54a427ab042c64", size = 10525001, upload-time = "2026-01-14T22:30:16.454Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a5/2f0b91894af13187110f9ad7ee926d86e4e6efa755c9c88a820ed7f84c85/ty-0.0.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00cf34c1ebe1147efeda3021a1064baa222c18cdac114b7b050bbe42deb4ca80", size = 10307103, upload-time = "2026-01-14T22:30:41.221Z" }, - { url = "https://files.pythonhosted.org/packages/4b/77/13d0410827e4bc713ebb7fdaf6b3590b37dcb1b82e0a81717b65548f2442/ty-0.0.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb3a655bd869352e9a22938d707631ac9fbca1016242b1f6d132d78f347c851", size = 10072737, upload-time = "2026-01-14T22:30:51.783Z" }, - { url = "https://files.pythonhosted.org/packages/e1/dd/fc36d8bac806c74cf04b4ca735bca14d19967ca84d88f31e121767880df1/ty-0.0.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4658e282c7cb82be304052f8f64f9925f23c3c4f90eeeb32663c74c4b095d7ba", size = 9368726, upload-time = "2026-01-14T22:30:18.683Z" }, - { url = "https://files.pythonhosted.org/packages/54/70/9e8e461647550f83e2fe54bc632ccbdc17a4909644783cdbdd17f7296059/ty-0.0.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c167d838eaaa06e03bb66a517f75296b643d950fbd93c1d1686a187e5a8dbd1f", size = 9454704, upload-time = "2026-01-14T22:30:22.759Z" }, - { url = "https://files.pythonhosted.org/packages/04/9b/6292cf7c14a0efeca0539cf7d78f453beff0475cb039fbea0eb5d07d343d/ty-0.0.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2956e0c9ab7023533b461d8a0e6b2ea7b78e01a8dde0688e8234d0fce10c4c1c", size = 9649829, upload-time = "2026-01-14T22:30:31.234Z" }, - { url = "https://files.pythonhosted.org/packages/49/bd/472a5d2013371e4870886cff791c94abdf0b92d43d305dd0f8e06b6ff719/ty-0.0.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c6a3fd7479580009f21002f3828320621d8a82d53b7ba36993234e3ccad58c8", size = 10162814, upload-time = "2026-01-14T22:30:36.174Z" }, - { url = "https://files.pythonhosted.org/packages/31/e9/2ecbe56826759845a7c21d80aa28187865ea62bc9757b056f6cbc06f78ed/ty-0.0.12-py3-none-win32.whl", hash = "sha256:a91c24fd75c0f1796d8ede9083e2c0ec96f106dbda73a09fe3135e075d31f742", size = 9140115, upload-time = "2026-01-14T22:30:38.903Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6d/d9531eff35a5c0ec9dbc10231fac21f9dd6504814048e81d6ce1c84dc566/ty-0.0.12-py3-none-win_amd64.whl", hash = "sha256:df151894be55c22d47068b0f3b484aff9e638761e2267e115d515fcc9c5b4a4b", size = 9884532, upload-time = "2026-01-14T22:30:25.112Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f3/20b49e75967023b123a221134548ad7000f9429f13fdcdda115b4c26305f/ty-0.0.12-py3-none-win_arm64.whl", hash = "sha256:cea99d334b05629de937ce52f43278acf155d3a316ad6a35356635f886be20ea", size = 9313974, upload-time = "2026-01-14T22:30:27.44Z" }, -] - -[[package]] -name = "types-requests" -version = "2.32.4.20260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, -] - -[[package]] -name = "types-tabulate" -version = "0.9.0.20241207" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/43/16030404a327e4ff8c692f2273854019ed36718667b2993609dc37d14dd4/types_tabulate-0.9.0.20241207.tar.gz", hash = "sha256:ac1ac174750c0a385dfd248edc6279fa328aaf4ea317915ab879a2ec47833230", size = 8195, upload-time = "2024-12-07T02:54:42.554Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/86/a9ebfd509cbe74471106dffed320e208c72537f9aeb0a55eaa6b1b5e4d17/types_tabulate-0.9.0.20241207-py3-none-any.whl", hash = "sha256:b8dad1343c2a8ba5861c5441370c3e35908edd234ff036d4298708a1d4cf8a85", size = 8307, upload-time = "2024-12-07T02:54:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/0ef15c22a1c54b0f728ceff3f62d478dbf8b0dcf8ff7b80b954f79584f3e/ty-0.0.17-py3-none-linux_armv6l.whl", hash = "sha256:64a9a16555cc8867d35c2647c2f1afbd3cae55f68fd95283a574d1bb04fe93e0", size = 10192793, upload-time = "2026-02-13T13:27:13.943Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/f4c322d9cded56edc016b1092c14b95cf58c8a33b4787316ea752bb9418e/ty-0.0.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eb2dbd8acd5c5a55f4af0d479523e7c7265a88542efe73ed3d696eb1ba7b6454", size = 10051977, upload-time = "2026-02-13T13:26:57.741Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a5/43746c1ff81e784f5fc303afc61fe5bcd85d0fcf3ef65cb2cef78c7486c7/ty-0.0.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f18f5fd927bc628deb9ea2df40f06b5f79c5ccf355db732025a3e8e7152801f6", size = 9564639, upload-time = "2026-02-13T13:26:42.781Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b8/280b04e14a9c0474af574f929fba2398b5e1c123c1e7735893b4cd73d13c/ty-0.0.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5383814d1d7a5cc53b3b07661856bab04bb2aac7a677c8d33c55169acdaa83df", size = 10061204, upload-time = "2026-02-13T13:27:00.152Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d7/493e1607d8dfe48288d8a768a2adc38ee27ef50e57f0af41ff273987cda0/ty-0.0.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c20423b8744b484f93e7bf2ef8a9724bca2657873593f9f41d08bd9f83444c9", size = 10013116, upload-time = "2026-02-13T13:26:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/80/ef/22f3ed401520afac90dbdf1f9b8b7755d85b0d5c35c1cb35cf5bd11b59c2/ty-0.0.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6f5b1aba97db9af86517b911674b02f5bc310750485dc47603a105bd0e83ddd", size = 10533623, upload-time = "2026-02-13T13:26:31.449Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/744b15279a11ac7138832e3a55595706b4a8a209c9f878e3ab8e571d9032/ty-0.0.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:488bce1a9bea80b851a97cd34c4d2ffcd69593d6c3f54a72ae02e5c6e47f3d0c", size = 11069750, upload-time = "2026-02-13T13:26:48.638Z" }, + { url = "https://files.pythonhosted.org/packages/f2/be/1133c91f15a0e00d466c24f80df486d630d95d1b2af63296941f7473812f/ty-0.0.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8df66b91ec84239420985ec215e7f7549bfda2ac036a3b3c065f119d1c06825a", size = 10870862, upload-time = "2026-02-13T13:26:54.715Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4a/a2ed209ef215b62b2d3246e07e833081e07d913adf7e0448fc204be443d6/ty-0.0.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:002139e807c53002790dfefe6e2f45ab0e04012e76db3d7c8286f96ec121af8f", size = 10628118, upload-time = "2026-02-13T13:26:45.439Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0c/87476004cb5228e9719b98afffad82c3ef1f84334bde8527bcacba7b18cb/ty-0.0.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6c4e01f05ce82e5d489ab3900ca0899a56c4ccb52659453780c83e5b19e2b64c", size = 10038185, upload-time = "2026-02-13T13:27:02.693Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/98f0b3ba9aef53c1f0305519536967a4aa793a69ed72677b0a625c5313ac/ty-0.0.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2b226dd1e99c0d2152d218c7e440150d1a47ce3c431871f0efa073bbf899e881", size = 10047644, upload-time = "2026-02-13T13:27:05.474Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/06737bb80aa1a9103b8651d2eb691a7e53f1ed54111152be25f4a02745db/ty-0.0.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8b11f1da7859e0ad69e84b3c5ef9a7b055ceed376a432fad44231bdfc48061c2", size = 10231140, upload-time = "2026-02-13T13:27:10.844Z" }, + { url = "https://files.pythonhosted.org/packages/7c/79/e2a606bd8852383ba9abfdd578f4a227bd18504145381a10a5f886b4e751/ty-0.0.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c04e196809ff570559054d3e011425fd7c04161529eb551b3625654e5f2434cb", size = 10718344, upload-time = "2026-02-13T13:26:51.66Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2d/2663984ac11de6d78f74432b8b14ba64d170b45194312852b7543cf7fd56/ty-0.0.17-py3-none-win32.whl", hash = "sha256:305b6ed150b2740d00a817b193373d21f0767e10f94ac47abfc3b2e5a5aec809", size = 9672932, upload-time = "2026-02-13T13:27:08.522Z" }, + { url = "https://files.pythonhosted.org/packages/de/b5/39be78f30b31ee9f5a585969930c7248354db90494ff5e3d0756560fb731/ty-0.0.17-py3-none-win_amd64.whl", hash = "sha256:531828267527aee7a63e972f54e5eee21d9281b72baf18e5c2850c6b862add83", size = 10542138, upload-time = "2026-02-13T13:27:17.084Z" }, + { url = "https://files.pythonhosted.org/packages/40/b7/f875c729c5d0079640c75bad2c7e5d43edc90f16ba242f28a11966df8f65/ty-0.0.17-py3-none-win_arm64.whl", hash = "sha256:de9810234c0c8d75073457e10a84825b9cd72e6629826b7f01c7a0b266ae25b1", size = 10023068, upload-time = "2026-02-13T13:26:39.637Z" }, ] [[package]] @@ -4986,9 +4258,6 @@ version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, @@ -5022,13 +4291,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/08/d5/25f7b19af3a2cb4000cac4f9e5525a40bec79f4f5d0ac9b517c0544586a0/xattr-1.3.0.tar.gz", hash = "sha256:30439fabd7de0787b27e9a6e1d569c5959854cb322f64ce7380fedbfa5035036", size = 17148, upload-time = "2025-10-13T22:16:47.353Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/64/292426ad5653e72c6e1325bbff22868a20077290d967cebb9c0624ad08b6/xattr-1.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:331a51bf8f20c27822f44054b0d760588462d3ed472d5e52ba135cf0bea510e8", size = 23448, upload-time = "2025-10-13T22:15:59.229Z" }, - { url = "https://files.pythonhosted.org/packages/63/84/6539fbe620da8e5927406e76b9c8abad8953025d5f578d792747c38a8c0e/xattr-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:196360f068b74fa0132a8c6001ce1333f095364b8f43b6fd8cdaf2f18741ef89", size = 18553, upload-time = "2025-10-13T22:16:00.151Z" }, - { url = "https://files.pythonhosted.org/packages/cc/bb/c1c2e24a49f8d13ff878fb85aabc42ea1b2f98ce08d8205b9661d517a9cc/xattr-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:405d2e4911d37f2b9400fa501acd920fe0c97fe2b2ec252cb23df4b59c000811", size = 18848, upload-time = "2025-10-13T22:16:01.046Z" }, - { url = "https://files.pythonhosted.org/packages/02/c2/a60aad150322b217dfe33695d8d9f32bc01e8f300641b6ba4b73f4b3c03f/xattr-1.3.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ae3a66ae1effd40994f64defeeaa97da369406485e60bfb421f2d781be3b75d", size = 38547, upload-time = "2025-10-13T22:16:01.973Z" }, - { url = "https://files.pythonhosted.org/packages/c6/58/2eca142bad4ea0a2be6b58d3122d0acce310c4e53fa7defd168202772178/xattr-1.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:69cd3bfe779f7ba87abe6473fdfa428460cf9e78aeb7e390cfd737b784edf1b5", size = 38753, upload-time = "2025-10-13T22:16:03.244Z" }, - { url = "https://files.pythonhosted.org/packages/2b/50/d032e5254c2c27d36bdb02abdf2735db6768a441f0e3d0f139e0f9f56638/xattr-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c5742ca61761a99ae0c522f90a39d5fb8139280f27b254e3128482296d1df2db", size = 38054, upload-time = "2025-10-13T22:16:04.656Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/458a306439aabe0083ca0a7b14c3e6a800ab9782b5ec0bdcec4ec9f3dc6c/xattr-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a04ada131e9bdfd32db3ab1efa9f852646f4f7c9d6fde0596c3825c67161be3", size = 37562, upload-time = "2025-10-13T22:16:05.97Z" }, { url = "https://files.pythonhosted.org/packages/bf/78/00bdc9290066173e53e1e734d8d8e1a84a6faa9c66aee9df81e4d9aeec1c/xattr-1.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dd4e63614722d183e81842cb237fd1cc978d43384166f9fe22368bfcb187ebe5", size = 23476, upload-time = "2025-10-13T22:16:06.942Z" }, { url = "https://files.pythonhosted.org/packages/53/16/5243722294eb982514fa7b6b87a29dfb7b29b8e5e1486500c5babaf6e4b3/xattr-1.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:995843ef374af73e3370b0c107319611f3cdcdb6d151d629449efecad36be4c4", size = 18556, upload-time = "2025-10-13T22:16:08.209Z" }, { url = "https://files.pythonhosted.org/packages/d6/5c/d7ab0e547bea885b55f097206459bd612cefb652c5fc1f747130cbc0d42c/xattr-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa23a25220e29d956cedf75746e3df6cc824cc1553326d6516479967c540e386", size = 18869, upload-time = "2025-10-13T22:16:10.319Z" }, @@ -5061,22 +4323,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, - { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, - { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, - { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, - { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, - { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, - { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, - { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, - { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, - { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, @@ -5102,23 +4348,6 @@ version = "0.25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, - { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, - { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, - { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, - { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, - { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, - { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, - { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, - { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, - { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, - { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" },