diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 631ea04840..0000000000 --- a/.dockerignore +++ /dev/null @@ -1,18 +0,0 @@ -**/.git -.DS_Store -*.dylib -*.DSYM -*.d -*.pyc -*.pyo -.*.swp -.*.swo -.*.un~ -*.tmp -*.o -*.o-* -*.os -*.os-* - -venv/ -.venv/ diff --git a/.gitattributes b/.gitattributes index efbe60f990..1cf541aa3a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,5 +10,5 @@ *.otf filter=lfs diff=lfs merge=lfs -text *.wav filter=lfs diff=lfs merge=lfs -text -selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text -system/hardware/tici/updater filter=lfs diff=lfs merge=lfs -text +openpilot/selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text +openpilot/common/hardware/tici/updater filter=lfs diff=lfs merge=lfs -text diff --git a/.github/labeler.yaml b/.github/labeler.yaml index 711f4597bd..e8d4057bb1 100644 --- a/.github/labeler.yaml +++ b/.github/labeler.yaml @@ -8,24 +8,24 @@ chore: car: - changed-files: - - any-glob-to-all-files: '{selfdrive/car/**,opendbc_repo}' + - any-glob-to-all-files: '{openpilot/selfdrive/car/**,opendbc_repo}' simulation: - changed-files: - - any-glob-to-all-files: 'tools/sim/**' + - any-glob-to-all-files: 'openpilot/tools/sim/**' ui: - changed-files: - - any-glob-to-all-files: '{selfdrive/assets/**,selfdrive/ui/**,system/ui/**}' + - any-glob-to-all-files: '{openpilot/selfdrive/assets/**,openpilot/selfdrive/ui/**,openpilot/system/ui/**}' tools: - changed-files: - - any-glob-to-all-files: 'tools/**' + - any-glob-to-all-files: 'openpilot/tools/**' multilanguage: - changed-files: - - any-glob-to-all-files: 'selfdrive/ui/translations/**' + - any-glob-to-all-files: 'openpilot/selfdrive/ui/translations/**' autonomy: - changed-files: - - any-glob-to-all-files: "{selfdrive/modeld/models/**,selfdrive/test/process_replay/model_replay_ref_commit,sunnypilot/modeld*/models/**}" + - any-glob-to-all-files: "{openpilot/selfdrive/modeld/models/**,openpilot/selfdrive/test/process_replay/model_replay_ref_commit,openpilot/sunnypilot/modeld*/models/**}" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2b4a5ed48f..b986273097 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -44,8 +44,8 @@ Explain how you tested this bug fix. **Checklist** -- [ ] added entry to CAR in selfdrive/car/*/values.py and ran `selfdrive/car/docs.py` to generate new docs -- [ ] test route added to [routes.py](https://github.com/commaai/openpilot/blob/master/selfdrive/car/tests/routes.py) +- [ ] added entry to CAR in openpilot/selfdrive/car/*/values.py and ran `openpilot/selfdrive/car/docs.py` to generate new docs +- [ ] test route added to [routes.py](https://github.com/commaai/openpilot/blob/master/openpilot/selfdrive/car/tests/routes.py) - [ ] route with openpilot: - [ ] route with stock system: - [ ] car harness used (if comma doesn't sell it, put N/A): diff --git a/.github/workflows/auto_pr_review.yaml b/.github/workflows/auto_pr_review.yaml index edb058d7d1..29149d8652 100644 --- a/.github/workflows/auto_pr_review.yaml +++ b/.github/workflows/auto_pr_review.yaml @@ -12,7 +12,7 @@ jobs: issues: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: submodules: false @@ -24,7 +24,7 @@ jobs: # Check PR target branch - name: check branch - uses: Vankka/pr-target-branch-action@def32ec9d93514138d6ac0132ee62e120a72aed5 + uses: Vankka/pr-target-branch-action@5da68a42bcb7b43d39104295a876a6f3f8d7908b if: github.repository == 'sunnypilot/sunnypilot' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/badges.yaml b/.github/workflows/badges.yaml deleted file mode 100644 index d170a96368..0000000000 --- a/.github/workflows/badges.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: badges -on: - schedule: - - cron: '0 * * * *' - workflow_dispatch: - -env: - PYTHONPATH: ${{ github.workspace }} - -jobs: - badges: - name: create badges - runs-on: ubuntu-latest - if: github.repository == 'sunnypilot/sunnypilot' - permissions: - contents: write - steps: - - uses: actions/checkout@v6 - with: - submodules: true - - run: ./tools/op.sh setup - - name: Push badges - run: | - python3 selfdrive/ui/translations/create_badges.py - - rm .gitattributes - - git checkout --orphan badges - git rm -rf --cached . - git config user.email "badge-researcher@sunnypilot.ai" - git config user.name "Badge Researcher" - - git add translation_badge.svg - git commit -m "Add/Update badges" - git push -f origin HEAD diff --git a/.github/workflows/build-all-tinygrad-models.yaml b/.github/workflows/build-all-tinygrad-models.yaml index 1e341cb8ec..0eedb04703 100644 --- a/.github/workflows/build-all-tinygrad-models.yaml +++ b/.github/workflows/build-all-tinygrad-models.yaml @@ -30,7 +30,7 @@ jobs: run: | cd sunnypilot export PYTHONPATH=$(pwd) - ref=$(python3 sunnypilot/models/tinygrad_ref.py) + ref=$(python3 openpilot/sunnypilot/models/tinygrad_ref.py) echo "tinygrad_ref=$ref" >> $GITHUB_OUTPUT echo "tinygrad_ref is $ref" diff --git a/.github/workflows/cereal_validation.yaml b/.github/workflows/cereal_validation.yaml index d71354dd59..0c87688bf1 100644 --- a/.github/workflows/cereal_validation.yaml +++ b/.github/workflows/cereal_validation.yaml @@ -6,7 +6,7 @@ on: - master pull_request: paths: - - 'cereal/**' + - 'openpilot/cereal/**' workflow_dispatch: workflow_call: inputs: @@ -30,36 +30,68 @@ jobs: - name: Checkout sunnypilot cereal uses: actions/checkout@v6 with: - sparse-checkout: cereal + sparse-checkout: | + openpilot/cereal + uv.lock + submodules: false - name: Init sunnypilot opendbc submodule run: git submodule update --init --depth 1 opendbc_repo - - name: Checkout upstream openpilot cereal + - name: Locate sunnypilot capnp import paths + id: locate-sp-capnp + run: | + SP_IMPORT_ARGS="" + SP_CAR_CAPNP=$(find opendbc_repo -maxdepth 4 -name car.capnp -path '*/opendbc/car/car.capnp' -printf '%h\n' -quit) + if [ -n "$SP_CAR_CAPNP" ]; then + SP_IMPORT_ARGS="-I $SP_CAR_CAPNP" + echo "Found sunnypilot car.capnp at: $SP_CAR_CAPNP" + fi + echo "import_args=$SP_IMPORT_ARGS" >> "$GITHUB_OUTPUT" + + - name: Checkout upstream openpilot uses: actions/checkout@v6 with: repository: 'commaai/openpilot' path: upstream_openpilot - sparse-checkout: cereal ref: "refs/heads/master" - name: Init upstream opendbc submodule working-directory: upstream_openpilot run: git submodule update --init --depth 1 opendbc_repo - - name: Install uv - run: pip install uv + - name: Locate upstream capnp paths + id: locate-capnp + run: | + CEREAL_DIR=$(find upstream_openpilot -maxdepth 4 -name log.capnp -path '*/cereal/log.capnp' -printf '%h\n' -quit) + if [ -z "$CEREAL_DIR" ]; then + echo "::error::Could not locate cereal/log.capnp in upstream openpilot" + exit 1 + fi + echo "cereal_dir=$CEREAL_DIR" >> "$GITHUB_OUTPUT" + echo "Found upstream cereal at: $CEREAL_DIR" + + IMPORT_ARGS="" + CAR_CAPNP=$(find upstream_openpilot -maxdepth 5 -name car.capnp -path '*/opendbc/car/car.capnp' -printf '%h\n' -quit) + if [ -n "$CAR_CAPNP" ]; then + IMPORT_ARGS="-I $CAR_CAPNP" + echo "Found car.capnp at: $CAR_CAPNP" + fi + echo "import_args=$IMPORT_ARGS" >> "$GITHUB_OUTPUT" + + - name: Install pycapnp + run: | + PYCAPNP_VER=$(python3 -c "import re; m=re.search(r'name = \"pycapnp\"\nversion = \"([^\"]+)\"', open('uv.lock').read()); print(m.group(1))") + pip install "pycapnp==${PYCAPNP_VER}" - name: Generate sunnypilot schema run: | - PYCAPNP_VER=$(python3 -c "import re; m=re.search(r'name = \"pycapnp\"\nversion = \"([^\"]+)\"', open('uv.lock').read()); print(m.group(1))") - uv run --isolated --with "pycapnp==${PYCAPNP_VER}" \ - python3 cereal/messaging/tests/validate_sp_cereal_upstream.py \ - -g -f /tmp/sp_schema.json --cereal-dir cereal + python3 openpilot/cereal/messaging/tests/validate_sp_cereal_upstream.py \ + -g -f /tmp/sp_schema.json --cereal-dir openpilot/cereal \ + ${{ steps.locate-sp-capnp.outputs.import_args }} - name: Validate against upstream run: | - PYCAPNP_VER=$(python3 -c "import re; m=re.search(r'name = \"pycapnp\"\nversion = \"([^\"]+)\"', open('uv.lock').read()); print(m.group(1))") - uv run --isolated --with "pycapnp==${PYCAPNP_VER}" \ - python3 cereal/messaging/tests/validate_sp_cereal_upstream.py \ - -r -f /tmp/sp_schema.json --cereal-dir upstream_openpilot/cereal + python3 openpilot/cereal/messaging/tests/validate_sp_cereal_upstream.py \ + -r -f /tmp/sp_schema.json --cereal-dir ${{ steps.locate-capnp.outputs.cereal_dir }} \ + ${{ steps.locate-capnp.outputs.import_args }} diff --git a/.github/workflows/diff_report.yaml b/.github/workflows/diff_report.yaml index 2ddb850944..0f706ea2ed 100644 --- a/.github/workflows/diff_report.yaml +++ b/.github/workflows/diff_report.yaml @@ -17,29 +17,41 @@ jobs: - name: Wait for process replay id: wait continue-on-error: true - uses: lewagon/wait-on-check-action@v1.3.4 + uses: lewagon/wait-on-check-action@1d57e2c51a58d812d2765e036a028b6bdb5a6154 with: ref: ${{ github.event.pull_request.head.sha }} check-name: process replay repo-token: ${{ secrets.GITHUB_TOKEN }} allowed-conclusions: success,failure wait-interval: 20 + - name: Get tests run ID + if: steps.wait.outcome == 'success' + id: tests-run + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + run_id=$(gh api \ + "repos/${{ github.repository }}/actions/workflows/tests.yaml/runs?event=pull_request&head_sha=${{ github.event.pull_request.head.sha }}&per_page=1" \ + --jq '.workflow_runs[0].id // empty') + if [ -z "$run_id" ]; then + echo "No tests.yaml run found for ${{ github.event.pull_request.head.sha }}" >&2 + exit 1 + fi + echo "run-id=$run_id" >> "$GITHUB_OUTPUT" - name: Download diff if: steps.wait.outcome == 'success' - uses: dawidd6/action-download-artifact@v6 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: - github_token: ${{ secrets.GITHUB_TOKEN }} - workflow: tests.yaml - workflow_conclusion: '' - pr: ${{ github.event.number }} + github-token: ${{ secrets.GITHUB_TOKEN }} + repository: ${{ github.repository }} + run-id: ${{ steps.tests-run.outputs.run-id }} name: diff_report_${{ github.event.number }} path: . - allow_forks: true - name: Comment on PR if: steps.wait.outcome == 'success' - uses: thollander/actions-comment-pull-request@v2 + uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b with: - filePath: diff_report.txt - comment_tag: diff_report - pr_number: ${{ github.event.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + file-path: diff_report.txt + comment-tag: diff_report + pr-number: ${{ github.event.number }} + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 23b20a86ff..0af94ba0da 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -22,7 +22,7 @@ jobs: steps: - uses: commaai/timeout@v1 - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: submodules: true @@ -34,7 +34,7 @@ jobs: python scripts/docs.py build # Push to docs.comma.ai - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 if: github.ref == 'refs/heads/master' && github.repository == 'sunnypilot/sunnypilot' with: path: openpilot-docs @@ -45,7 +45,7 @@ jobs: run: | set -x - source release/identity.sh + source tools/release/identity.sh cd openpilot-docs git checkout --orphan tmp diff --git a/.github/workflows/jenkins-pr-trigger.yaml b/.github/workflows/jenkins-pr-trigger.yaml index fdd26253fa..ebb14d5dba 100644 --- a/.github/workflows/jenkins-pr-trigger.yaml +++ b/.github/workflows/jenkins-pr-trigger.yaml @@ -11,7 +11,7 @@ jobs: contents: write steps: - name: Delete stale Jenkins branches - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const cutoff = Date.now() - 24 * 60 * 60 * 1000; @@ -52,7 +52,7 @@ jobs: steps: - name: Check for trigger phrase id: check_comment - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const triggerPhrase = "trigger-jenkins"; @@ -72,7 +72,7 @@ jobs: - name: Checkout repository if: steps.check_comment.outputs.result == 'true' - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: refs/pull/${{ github.event.issue.number }}/head @@ -86,7 +86,7 @@ jobs: - name: Delete trigger comment if: steps.check_comment.outputs.result == 'true' && always() - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | await github.rest.issues.deleteComment({ diff --git a/.github/workflows/lfs-maintenance.yaml b/.github/workflows/lfs-maintenance.yaml index 8780abfbb3..3b92996f82 100644 --- a/.github/workflows/lfs-maintenance.yaml +++ b/.github/workflows/lfs-maintenance.yaml @@ -65,8 +65,21 @@ jobs: echo ' pushurl = ${{ env.LFS_PUSH_URL }}' >> .lfsconfig echo ' locksverify = false' >> .lfsconfig + - name: Configure LFS transfer settings + run: | + git config lfs.activitytimeout 300 + git config lfs.transfer.maxretries 5 + git config lfs.concurrenttransfers 4 + - name: Push LFS id: sync-and-commit run: | git lfs ls-files -l - git lfs push --all origin \ No newline at end of file + for attempt in 1 2 3; do + echo "Push attempt $attempt..." + git lfs push --all origin && exit 0 + echo "Attempt $attempt failed, retrying in 30s..." + sleep 30 + done + echo "All push attempts failed" + exit 1 diff --git a/.github/workflows/model_review.yaml b/.github/workflows/model_review.yaml index 2775dbc574..82c732dc3b 100644 --- a/.github/workflows/model_review.yaml +++ b/.github/workflows/model_review.yaml @@ -4,7 +4,7 @@ on: pull_request: types: [opened, reopened, synchronize] paths: - - 'selfdrive/modeld/models/*.onnx' + - 'openpilot/selfdrive/modeld/models/*.onnx' workflow_dispatch: jobs: @@ -16,11 +16,11 @@ jobs: if: github.repository == 'commaai/openpilot' steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true - name: Checkout master - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: master path: base @@ -36,7 +36,7 @@ jobs: echo "EOF" >> $GITHUB_OUTPUT - name: Post model report comment - uses: marocchino/sticky-pull-request-comment@baa7203ed60924babbe5dcd0ac8eae3b66ec5e16 + uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 with: header: model-review message: ${{ steps.report.outputs.content }} \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6ae5336557..56aa126684 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -15,17 +15,17 @@ jobs: steps: - name: Wait for green check mark if: ${{ github.event_name == 'schedule' }} - uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc + uses: lewagon/wait-on-check-action@1d57e2c51a58d812d2765e036a028b6bdb5a6154 with: ref: master wait-interval: 30 running-workflow-name: 'build __nightly' repo-token: ${{ secrets.GITHUB_TOKEN }} check-regexp: ^((?!.*(build prebuilt|create badges).*).)*$ - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 # checkout version > v4 breaks nightly release script with: submodules: true fetch-depth: 0 - run: ./tools/op.sh setup - name: Push __nightly - run: BRANCH=__nightly release/build_stripped.sh + run: BRANCH=__nightly tools/release/build_stripped.sh diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index 0db4933649..87556b4011 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -3,6 +3,8 @@ name: repo maintenance on: schedule: - cron: "0 14 * * 1" # every Monday at 2am UTC (6am PST) + pull_request: + types: [closed] workflow_dispatch: env: @@ -12,9 +14,11 @@ jobs: package_updates: name: package_updates runs-on: ubuntu-latest - if: github.repository == 'sunnypilot/sunnypilot' + if: >- + github.repository == 'sunnypilot/sunnypilot' && + (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: submodules: true - run: ./tools/op.sh setup @@ -50,10 +54,10 @@ jobs: git add . - name: update car docs run: | - python selfdrive/car/docs.py + python openpilot/selfdrive/car/docs.py git add docs/CARS.md - name: Create Pull Request - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 with: author: github-actions[bot] token: ${{ github.repository == 'commaai/openpilot' && secrets.ACTIONS_CREATE_PR_PAT || secrets.GITHUB_TOKEN }} @@ -75,3 +79,51 @@ jobs: ${{ steps.pip_tree.outputs.PIP_TREE }} ``` labels: bot + + cleanup_closed_branches: + if: github.repository == 'commaai/openpilot' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - uses: actions/github-script@v9 + with: + script: | + const { owner, repo } = context.repo; + const upstream = `${owner}/${repo}`; + + const closed = context.payload.pull_request; + if (closed) { + if (closed.head.repo?.full_name === upstream) { + await github.rest.git.deleteRef({ owner, repo, ref: `heads/${closed.head.ref}` }).catch(console.log); + } + return; + } + + for await (const response of github.paginate.iterator(github.rest.pulls.list, { + owner, + repo, + state: 'closed', + per_page: 100, + })) { + for (const pr of response.data) { + if (pr.head.repo?.full_name !== upstream) continue; + + const branch = pr.head.ref; + try { + await github.rest.git.deleteRef({ + owner, + repo, + ref: `heads/${branch}`, + }); + console.log(`Deleted branch ${branch} (PR #${pr.number})`); + } catch (error) { + if (error.status === 422 || error.status === 403) { + console.log(`Skipping branch ${branch} (PR #${pr.number}): ${error.message}`); + continue; + } + throw error; + } + } + } diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index c092532712..26dc5f081f 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -5,7 +5,7 @@ env: OUTPUT_DIR: ${{ github.workspace }}/output SCONS_CACHE_DIR: ${{ github.workspace }}/release/ci/scons_cache UPSTREAM_REPO: "commaai/openpilot" - TINYGRAD_PATH: ${{ github.workspace }}/openpilot/tinygrad_repo + TINYGRAD_PATH: ${{ github.workspace }}/tinygrad_repo MODELS_DIR: ${{ github.workspace }}/openpilot/selfdrive/modeld/models on: @@ -141,7 +141,10 @@ jobs: # Set up common environment source /etc/profile; export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export UV_PYTHON_PREFERENCE=managed + export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + uv sync printenv >> $GITHUB_ENV if [[ "${{ runner.debug }}" == "1" ]]; then cat $GITHUB_OUTPUT @@ -161,6 +164,7 @@ jobs: if [[ "${{ runner.debug }}" == "1" ]]; then printenv fi + source ${UV_PROJECT_ENVIRONMENT}/bin/activate PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable rm -rf ${{ env.MODELS_DIR }}/*.onnx @@ -168,9 +172,9 @@ jobs: uses: actions/download-artifact@v4 with: name: models-${{ env.REF }}${{ inputs.artifact_suffix }} - path: ${{ github.workspace }}/openpilot/selfdrive/modeld/models + path: ${{ env.MODELS_DIR }} - run: | - rm -f ${{ github.workspace }}/openpilot/selfdrive/modeld/models/{dmonitoring_model,big_driving_policy,big_driving_vision,big_driving_supercombo}.onnx + rm -f ${{ env.MODELS_DIR }}/{dmonitoring_model,big_driving_policy,big_driving_vision}.onnx - name: Build Model run: | @@ -279,4 +283,5 @@ jobs: - name: Re-enable powersave if: always() run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 977dc94972..12a3a7bce0 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -79,7 +79,7 @@ jobs: is_stable_branch="$(echo "$CONFIG" | jq -r '.stable_branch // false')"; echo "is_stable_branch=$is_stable_branch" >> $GITHUB_OUTPUT - stable_version=$(cat sunnypilot/common/version.h | grep SUNNYPILOT_VERSION | sed -e 's/[^0-9|.]//g'); + stable_version=$(cat openpilot/sunnypilot/common/version.h | grep SUNNYPILOT_VERSION | sed -e 's/[^0-9|.]//g'); echo "version=$([ "$is_stable_branch" = "true" ] && echo "$stable_version" || echo "$BUILD")" >> $GITHUB_OUTPUT echo "extra_version_identifier=${environment}" >> $GITHUB_OUTPUT fi @@ -152,7 +152,10 @@ jobs: # Set up common environment source /etc/profile; export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export UV_PYTHON_PREFERENCE=managed + export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + uv sync printenv >> $GITHUB_ENV if [[ "${{ runner.debug }}" == "1" ]]; then cat $GITHUB_OUTPUT @@ -172,20 +175,26 @@ jobs: if [[ "${{ runner.debug }}" == "1" ]]; then printenv fi + source ${UV_PROJECT_ENVIRONMENT}/bin/activate PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable - name: Build Main Project run: | export PYTHONPATH="$BUILD_DIR" - ./release/release_files.py | sort | uniq | rsync -rRl${RUNNER_DEBUG:+v} --files-from=- . $BUILD_DIR/ + ./tools/release/release_files.py | sort | uniq | rsync -rRl${RUNNER_DEBUG:+v} --files-from=- . $BUILD_DIR/ cd $BUILD_DIR + ln -sfn msgq_repo/msgq msgq + ln -sfn opendbc_repo/opendbc opendbc + ln -sfn rednose_repo/rednose rednose + ln -sfn teleoprtc_repo/teleoprtc teleoprtc + ln -sfn tinygrad_repo/tinygrad tinygrad sed -i '/from .board.jungle import PandaJungle, PandaJungleDFU/s/^/#/' panda/__init__.py echo "Building sunnypilot's modeld_v2..." - scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal sunnypilot/modeld_v2 + scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal openpilot/sunnypilot/modeld_v2 echo "Building sunnypilot's locationd..." - scons -j2 cache_dir=${{env.SCONS_CACHE_DIR}} --minimal sunnypilot/selfdrive/locationd + scons -j2 cache_dir=${{env.SCONS_CACHE_DIR}} --minimal openpilot/sunnypilot/selfdrive/locationd echo "Building openpilot's locationd..." - scons -j1 cache_dir=${{env.SCONS_CACHE_DIR}} --minimal selfdrive/locationd + scons -j1 cache_dir=${{env.SCONS_CACHE_DIR}} --minimal openpilot/selfdrive/locationd echo "Building rest of sunnypilot" scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal touch ${BUILD_DIR}/prebuilt @@ -208,17 +217,17 @@ jobs: --exclude='Jenkinsfile' \ --exclude='**/release/' \ --exclude='**/.github/' \ - --exclude='**/selfdrive/ui/replay/' \ + --exclude='**/openpilot/selfdrive/ui/replay/' \ --exclude='**/__pycache__/' \ --exclude='${{env.SCONS_CACHE_DIR}}' \ --exclude='**/.git/' \ --exclude='**/SConstruct' \ --exclude='**/SConscript' \ --exclude='**/.venv/' \ - --exclude='selfdrive/modeld/models/*.onnx*' \ - --exclude='sunnypilot/modeld*/models/*.onnx*' \ - --exclude='third_party/*x86*' \ - --exclude='third_party/*Darwin*' \ + --exclude='openpilot/selfdrive/modeld/models/*.onnx*' \ + --exclude='openpilot/sunnypilot/modeld*/models/*.onnx*' \ + --exclude='openpilot/third_party/*x86*' \ + --exclude='openpilot/third_party/*Darwin*' \ --delete-excluded \ --chown=comma:comma \ ${BUILD_DIR}/ ${OUTPUT_DIR}/ @@ -237,6 +246,7 @@ jobs: - name: Re-enable powersave if: always() run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable @@ -363,7 +373,7 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, - name: process.env.LABEL + name: process.env.LABELf }); console.log(`Removed '${process.env.LABEL}' label from PR #${prNumber}`); diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index ee8f61f880..b5f941fc76 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -35,27 +35,27 @@ jobs: STRIPPED_DIR: /tmp/releasepilot PYTHONPATH: /tmp/releasepilot steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: submodules: true - name: Getting LFS files - uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 with: timeout_minutes: 2 max_attempts: 3 command: git lfs pull - name: Build devel - timeout-minutes: 1 - run: TARGET_DIR=$STRIPPED_DIR release/build_stripped.sh + timeout-minutes: 3 + run: TARGET_DIR=$STRIPPED_DIR tools/release/build_stripped.sh - run: ./tools/op.sh setup - name: Build openpilot and run checks timeout-minutes: 30 working-directory: ${{ env.STRIPPED_DIR }} - run: python3 system/manager/build.py + run: python3 openpilot/system/manager/build.py - name: Run tests timeout-minutes: 1 working-directory: ${{ env.STRIPPED_DIR }} - run: release/check-dirty.sh + run: tools/release/check-dirty.sh - name: Check submodules if: github.repository == 'sunnypilot/sunnypilot' timeout-minutes: 3 @@ -75,15 +75,13 @@ jobs: export CHECK_PR_REFS=true fi fi - release/check-submodules.sh + tools/release/check-submodules.sh build_mac: name: build macOS 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@v6 - with: - submodules: true + - uses: actions/checkout@v7 - name: Remove Homebrew from environment run: | FILTERED=$(echo "$PATH" | tr ':' '\n' | grep -v '/opt/homebrew' | tr '\n' ':') @@ -101,9 +99,7 @@ jobs: && fromJSON('["namespace-profile-amd64-8x16"]') || fromJSON('["ubuntu-24.04"]') }} steps: - - uses: actions/checkout@v6 - with: - submodules: true + - uses: actions/checkout@v7 - run: ./tools/op.sh setup - name: Static analysis timeout-minutes: 1 @@ -118,16 +114,18 @@ jobs: && fromJSON('["namespace-profile-amd64-8x16"]') || fromJSON('["ubuntu-24.04"]') }} steps: - - uses: actions/checkout@v6 - with: - submodules: true + - uses: actions/checkout@v7 - run: ./tools/op.sh setup + - name: Install raylib headless dependencies + if: ${{ !contains(runner.name, 'nsc') }} + run: sudo apt-get install -y --no-install-recommends libgles2 libegl1 - name: Build openpilot run: scons - name: Run unit tests timeout-minutes: ${{ contains(runner.name, 'nsc') && 2 || 999 }} + env: + RAYLIB_BACKEND: headless run: | - source selfdrive/test/setup_xvfb.sh # Pre-compile Python bytecode so each pytest worker doesn't need to $PYTEST --collect-only -m 'not slow' -qq MAX_EXAMPLES=1 $PYTEST -m 'not slow' @@ -142,39 +140,37 @@ jobs: && fromJSON('["namespace-profile-amd64-8x16"]') || fromJSON('["ubuntu-24.04"]') }} steps: - - uses: actions/checkout@v6 - with: - submodules: true + - uses: actions/checkout@v7 - run: ./tools/op.sh setup - name: Build openpilot run: scons - name: Run replay timeout-minutes: ${{ contains(runner.name, 'nsc') && 2 || 20 }} continue-on-error: ${{ github.ref == 'refs/heads/master' }} - run: selfdrive/test/process_replay/test_processes.py -j$(nproc) + run: openpilot/selfdrive/test/process_replay/test_processes.py -j$(nproc) - name: Print diff id: print-diff if: always() - run: cat selfdrive/test/process_replay/diff.txt + run: cat openpilot/selfdrive/test/process_replay/diff.txt - name: Print diff report if: always() - run: cat selfdrive/test/process_replay/diff_report.txt - - uses: actions/upload-artifact@v6 + run: cat openpilot/selfdrive/test/process_replay/diff_report.txt + - uses: actions/upload-artifact@v7 if: always() continue-on-error: true with: name: process_replay_diff.txt - path: selfdrive/test/process_replay/diff.txt + path: openpilot/selfdrive/test/process_replay/diff.txt - name: Upload diff report - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() && github.event_name == 'pull_request' continue-on-error: true with: name: diff_report_${{ github.event.number }} - path: selfdrive/test/process_replay/diff_report.txt + path: openpilot/selfdrive/test/process_replay/diff_report.txt - name: Checkout ci-artifacts if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master' - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: commaai/ci-artifacts ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} @@ -187,13 +183,13 @@ jobs: git config user.email "<>" git fetch origin process-replay || true git checkout process-replay 2>/dev/null || git checkout --orphan process-replay - cp ${{ github.workspace }}/selfdrive/test/process_replay/fakedata/*.zst . + cp ${{ github.workspace }}/openpilot/selfdrive/test/process_replay/fakedata/*.zst . echo "${{ github.sha }}" > ref_commit git add . git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit" - name: Push refs if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master' - uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 with: timeout_minutes: 2 max_attempts: 3 @@ -203,7 +199,7 @@ jobs: timeout-minutes: 4 env: ONNXCPU: 1 - run: $PYTEST selfdrive/test/process_replay/test_regen.py + run: $PYTEST openpilot/selfdrive/test/process_replay/test_regen.py simulator_driving: name: simulator driving @@ -215,17 +211,16 @@ jobs: || fromJSON('["ubuntu-24.04"]') }} if: false # FIXME: Started to timeout recently steps: - - uses: actions/checkout@v6 - with: - submodules: true + - uses: actions/checkout@v7 - run: ./tools/op.sh setup - name: Build openpilot run: scons - name: Driving test timeout-minutes: 2 - run: | - source selfdrive/test/setup_xvfb.sh - pytest -s tools/sim/tests/test_metadrive_bridge.py + env: + # MetaDrive renders offscreen through panda3d's EGL pipe on llvmpipe + EGL_PLATFORM: surfaceless + run: pytest -s openpilot/tools/sim/tests/test_metadrive_bridge.py create_ui_report: name: Create UI Report @@ -236,19 +231,21 @@ jobs: && fromJSON('["namespace-profile-amd64-8x16"]') || fromJSON('["ubuntu-24.04"]') }} steps: - - uses: actions/checkout@v6 - with: - submodules: true + - uses: actions/checkout@v7 - run: ./tools/op.sh setup + - name: Install raylib headless dependencies + if: ${{ !contains(runner.name, 'nsc') }} + run: sudo apt-get install -y --no-install-recommends libgles2 libegl1 - name: Build openpilot run: scons - name: Create UI Report + env: + RAYLIB_BACKEND: headless run: | - source selfdrive/test/setup_xvfb.sh - python3 selfdrive/ui/tests/diff/replay.py - python3 selfdrive/ui/tests/diff/replay.py --big + python3 openpilot/selfdrive/ui/tests/diff/replay.py + python3 openpilot/selfdrive/ui/tests/diff/replay.py --big - name: Upload UI Report - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ui-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} - path: selfdrive/ui/tests/diff/report + path: openpilot/selfdrive/ui/tests/diff/report diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index a02726c3af..8b7b63f344 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -8,9 +8,9 @@ on: branches: - 'master' paths: - - 'selfdrive/assets/**' - - 'selfdrive/ui/**' - - 'system/ui/**' + - 'openpilot/selfdrive/assets/**' + - 'openpilot/selfdrive/ui/**' + - 'openpilot/system/ui/**' workflow_dispatch: env: @@ -34,12 +34,12 @@ jobs: pull-requests: write actions: read steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: submodules: true - name: Waiting for ui generation to end - uses: lewagon/wait-on-check-action@v1.3.4 + uses: lewagon/wait-on-check-action@1d57e2c51a58d812d2765e036a028b6bdb5a6154 with: ref: ${{ env.SHA }} check-name: ${{ env.UI_JOB_NAME }} @@ -53,7 +53,7 @@ jobs: echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?[0-9]+)") | .number')" >> $GITHUB_OUTPUT - name: Getting proposed ui - uses: dawidd6/action-download-artifact@v6 + uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 with: github_token: ${{ secrets.GITHUB_TOKEN }} run_id: ${{ steps.get_run_id.outputs.run_id }} @@ -62,7 +62,7 @@ jobs: path: ${{ github.workspace }}/pr_ui - name: Getting mici master ui - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: sunnypilot/ci-artifacts ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} @@ -70,7 +70,7 @@ jobs: ref: openpilot_master_ui_mici_raylib - name: Getting big master ui - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: sunnypilot/ci-artifacts ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} @@ -97,7 +97,7 @@ jobs: done - name: Setup FFmpeg - uses: AnimMouse/setup-ffmpeg@ae28d57dabbb148eff63170b6bf7f2b60062cbae + uses: AnimMouse/setup-ffmpeg@178e0b4a408f06ce40d896c3cd79f50fa6f8b0c3 - name: Finding diffs if: github.event_name == 'pull_request_target' @@ -115,13 +115,13 @@ jobs: cp "${{ github.workspace }}/master_${name}/${video}.mp4" "${{ github.workspace }}/pr_ui/${video}_master.mp4" diff_exit_code=0 - python3 ${{ github.workspace }}/selfdrive/ui/tests/diff/diff.py \ + python3 ${{ github.workspace }}/openpilot/selfdrive/ui/tests/diff/diff.py \ "${{ github.workspace }}/pr_ui/${video}_master.mp4" \ "${{ github.workspace }}/pr_ui/${video}_proposed.mp4" \ "${diff_name}.html" --basedir "$baseurl" --no-open || diff_exit_code=$? - cp "${{ github.workspace }}/selfdrive/ui/tests/diff/report/${diff_name}.html" "${{ github.workspace }}/pr_ui/" - cp "${{ github.workspace }}/selfdrive/ui/tests/diff/report/${diff_name}.mp4" "${{ github.workspace }}/pr_ui/" + cp "${{ github.workspace }}/openpilot/selfdrive/ui/tests/diff/report/${diff_name}.html" "${{ github.workspace }}/pr_ui/" + cp "${{ github.workspace }}/openpilot/selfdrive/ui/tests/diff/report/${diff_name}.mp4" "${{ github.workspace }}/pr_ui/" REPORT_URL="https://sunnypilot.github.io/ci-artifacts/${diff_name}_pr_${{ github.event.number }}.html" if [ $diff_exit_code -eq 0 ]; then @@ -156,7 +156,7 @@ jobs: for variant in $VARIANTS; do IFS=':' read -r name _ _ <<< "$variant" diff_name="${name}_diff" - cp "${{ github.workspace }}/selfdrive/ui/tests/diff/report/${diff_name}.html" "${diff_name}_pr_${{ github.event.number }}.html" + cp "${{ github.workspace }}/openpilot/selfdrive/ui/tests/diff/report/${diff_name}.html" "${diff_name}_pr_${{ github.event.number }}.html" git add "${diff_name}_pr_${{ github.event.number }}.html" done git commit -m "ui diff reports for PR #${{ github.event.number }}" || echo "No changes to commit" @@ -164,12 +164,12 @@ jobs: - name: Comment on PR if: github.event_name == 'pull_request_target' - uses: thollander/actions-comment-pull-request@v2 + uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b with: message: | ## UI Preview ${{ steps.find_diff.outputs.COMMENT }} - comment_tag: run_id_ui_preview - pr_number: ${{ github.event.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + comment-tag: run_id_ui_preview + pr-number: ${{ github.event.number }} + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index e4eaeb2c50..a033f70507 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,13 @@ a.out .cache/ bin/ +# created at launch for TICI PYTHONPATH (PC uses editable installs via pyproject.toml) +/msgq +/opendbc +/rednose +/teleoprtc +/tinygrad + *.mp4 *.dylib *.DSYM @@ -44,18 +51,18 @@ bin/ config.json compile_commands.json compare_runtime*.html -selfdrive/modeld/models/tg_input_devices.json +openpilot/selfdrive/modeld/models/tg_input_devices.json # build artifacts docs_site/ -selfdrive/pandad/pandad -cereal/services.h -cereal/gen -cereal/messaging/bridge -selfdrive/ui/translations/tmp -selfdrive/car/tests/cars_dump -system/camerad/camerad -system/camerad/test/ae_gray_test +openpilot/selfdrive/pandad/pandad +openpilot/cereal/services.h +openpilot/cereal/gen +openpilot/cereal/messaging/bridge +openpilot/selfdrive/ui/translations/tmp +openpilot/selfdrive/car/tests/cars_dump +openpilot/system/camerad/camerad +openpilot/system/camerad/test/ae_gray_test .coverage* coverage.xml diff --git a/.gitmodules b/.gitmodules index 5c5d72a7dc..a2d892d07f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,7 +6,7 @@ url = https://github.com/sunnypilot/opendbc.git [submodule "msgq"] path = msgq_repo - url = https://github.com/commaai/msgq.git + url = https://github.com/sunnypilot/msgq.git [submodule "rednose_repo"] path = rednose_repo url = https://github.com/commaai/rednose.git @@ -17,5 +17,5 @@ path = tinygrad_repo url = https://github.com/sunnypilot/tinygrad.git [submodule "sunnypilot/neural_network_data"] - path = sunnypilot/neural_network_data + path = openpilot/sunnypilot/neural_network_data url = https://github.com/sunnypilot/neural-network-data.git diff --git a/.vscode/launch.json b/.vscode/launch.json index 151b757dab..5a052a112a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,9 +6,9 @@ "type": "pickString", "description": "Select the process to debug", "options": [ - "selfdrive/controls/controlsd.py", - "system/timed/timed.py", - "tools/sim/run_bridge.py" + "openpilot/selfdrive/controls/controlsd.py", + "openpilot/system/timed/timed.py", + "openpilot/tools/sim/run_bridge.py" ] }, { @@ -16,7 +16,7 @@ "type": "pickString", "description": "Select the process to debug", "options": [ - "selfdrive/ui/ui" + "openpilot/selfdrive/ui/ui" ] }, { diff --git a/.vscode/settings.json b/.vscode/settings.json index 811306f399..68f6b90371 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,10 +17,5 @@ "**/.git", "**/.venv", "**/__pycache__", - // exclude directories that should be using the symlinked version - "common/**", - "selfdrive/**", - "system/**", - "tools/**", ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index e5e31c60ff..aeb2a969c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ -sunnypilot Version 2026.002.000 (2026-xx-xx) +sunnypilot Version 2026.002.000 (2026-06-28) ======================== +* What's Changed (sunnypilot/sunnypilot) + * ui: update gates for certain toggles by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1830 + * release: ignore upstream IsReleaseBranch by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1831 + * manager: disable DEVELOPMENT_ONLY reset by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1833 + * sunnylink: fix max time offroad values by @nayan8teen in https://github.com/sunnypilot/sunnypilot/pull/1835 + * ui: show default model name by @nayan8teen in https://github.com/sunnypilot/sunnypilot/pull/1837 + * sunnylink: add CarParams fallback for brand-specific capabilities by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1839 + * sunnylink SDUI: tweak DisableUpdate param for clarity by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1842 + * Revert "DM: Lancia Delta HF Integrale model" by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1849 + * modeld_v2: safe model validation by @Discountchubbs in https://github.com/sunnypilot/sunnypilot/pull/1855 + * Revert "deprecate `carState.brake`" for Honda Gas Interceptor by @mvl-boston in https://github.com/sunnypilot/sunnypilot/pull/1860 + * sunnylink: deprecate legacy params metadata by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1862 + * ui: reset Enforce Torque Control and NNLC if both are enabled by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1863 +* What's Changed (sunnypilot/opendbc) + * Rivian: suppress ACM hold-the-wheel warning during MADS-only lateral by @lukasloetkolben in https://github.com/sunnypilot/opendbc/pull/465 + * Sync: `commaai/opendbc:master` → `sunnypilot/opendbc:master` by @sunnyhaibin in https://github.com/sunnypilot/opendbc/pull/479 + * safety: add option to ignore frequency check for RX checks by @sunnyhaibin in https://github.com/sunnypilot/opendbc/pull/480 + * Revert "deprecate carState.brake" for Honda Gas Interceptor by @mvl-boston in https://github.com/sunnypilot/opendbc/pull/481 +* New Contributors (sunnypilot/sunnypilot) + * @mvl-boston made their first contribution in https://github.com/sunnypilot/sunnypilot/pull/1860 +* Full Changelog: https://github.com/sunnypilot/sunnypilot/compare/v2026.001.007...v2026.002.000 +************************ +* Synced with commaai's openpilot (v0.11.1) + * master commit 69e2c321e49760e52f7983eaa0a5f77cb95de637 (June 02, 2026) +* New driver monitoring model +* Improved image processing pipeline for driver camera +* Improved thermal policy for comma four +* Acura MDX 2022-24 support thanks to mvl-boston! +* Rivian R1S and R1T 2025 support thanks to lukasloetkolben! sunnypilot Version 2026.001.000 (2026-05-06) ======================== diff --git a/Dockerfile.openpilot b/Dockerfile.openpilot deleted file mode 100644 index 72d874b022..0000000000 --- a/Dockerfile.openpilot +++ /dev/null @@ -1,38 +0,0 @@ -FROM ubuntu:24.04 - -ENV PYTHONUNBUFFERED=1 - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && \ - apt-get install -y --no-install-recommends sudo tzdata locales && \ - rm -rf /var/lib/apt/lists/* - -RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen -ENV LANG=en_US.UTF-8 -ENV LANGUAGE=en_US:en -ENV LC_ALL=en_US.UTF-8 - -ENV NVIDIA_VISIBLE_DEVICES=all -ENV NVIDIA_DRIVER_CAPABILITIES=graphics,utility,compute - -ARG USER=batman -ARG USER_UID=1001 -RUN useradd -m -s /bin/bash -u $USER_UID $USER -RUN usermod -aG sudo $USER -RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers -USER $USER - -ENV OPENPILOT_PATH=/home/$USER/openpilot -RUN mkdir -p ${OPENPILOT_PATH} -WORKDIR ${OPENPILOT_PATH} - -COPY --chown=$USER . ${OPENPILOT_PATH}/ - -ENV UV_BIN="/home/$USER/.local/bin/" -ENV VIRTUAL_ENV=${OPENPILOT_PATH}/.venv -ENV PATH="$UV_BIN:$VIRTUAL_ENV/bin:$PATH" -RUN tools/setup_dependencies.sh && \ - sudo rm -rf /var/lib/apt/lists/* - -USER root -RUN git config --global --add safe.directory '*' diff --git a/Jenkinsfile b/Jenkinsfile index a7f46ce17a..d57e9502aa 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -12,7 +12,7 @@ def retryWithDelay(int maxRetries, int delay, Closure body) { def device(String ip, String step_label, String cmd) { withCredentials([file(credentialsId: 'id_rsa', variable: 'key_file')]) { def ssh_cmd = """ -ssh -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=2 -o BatchMode=yes -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' exec /usr/bin/bash <<'END' +ssh -o ControlMaster=auto -o ControlPath=/tmp/ssh_control_%C -o ControlPersist=yes -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=2 -o BatchMode=yes -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' exec /usr/bin/bash <<'END' set -e @@ -63,6 +63,8 @@ if [ -f /data/openpilot/launch_env.sh ]; then source /data/openpilot/launch_env.sh fi +export LD_LIBRARY_PATH="\$(python -c 'import ffmpeg; print(ffmpeg.LIB_DIR)'):/usr/local/lib:\${LD_LIBRARY_PATH:-}" + ln -snf ${env.TEST_DIR} /data/pythonpath cd ${env.TEST_DIR} || true @@ -93,7 +95,7 @@ def deviceStage(String stageName, String deviceType, List extra_env, def steps) retry (3) { def date = sh(script: 'date', returnStdout: true).trim(); device(device_ip, "set time", "date -s '" + date + "'") - device(device_ip, "git checkout", extra + "\n" + readFile("selfdrive/test/setup_device_ci.sh")) + device(device_ip, "git checkout", extra + "\n" + readFile("openpilot/selfdrive/test/setup_device_ci.sh")) } steps.each { item -> def name = item[0] @@ -179,7 +181,7 @@ node { try { if (env.BRANCH_NAME == 'devel-staging') { deviceStage("build release-tizi-staging", "tizi-needs-can", [], [ - step("build release-tizi-staging", "RELEASE_BRANCH=release-tizi-staging,release-mici-staging $SOURCE_DIR/release/build_release.sh"), + step("build release-tizi-staging", "RELEASE_BRANCH=release-tizi-staging,release-mici-staging $SOURCE_DIR/tools/release/build_release.sh"), ]) } @@ -187,12 +189,12 @@ node { parallel ( 'nightly': { deviceStage("build nightly", "tizi-needs-can", [], [ - step("build nightly", "RELEASE_BRANCH=nightly $SOURCE_DIR/release/build_release.sh"), + step("build nightly", "RELEASE_BRANCH=nightly $SOURCE_DIR/tools/release/build_release.sh"), ]) }, 'nightly-dev': { deviceStage("build nightly-dev", "tizi-needs-can", [], [ - step("build nightly-dev", "PANDA_DEBUG_BUILD=1 RELEASE_BRANCH=nightly-dev $SOURCE_DIR/release/build_release.sh"), + step("build nightly-dev", "PANDA_DEBUG_BUILD=1 RELEASE_BRANCH=nightly-dev $SOURCE_DIR/tools/release/build_release.sh"), ]) }, ) @@ -202,51 +204,51 @@ node { parallel ( 'onroad tests': { deviceStage("onroad", "tizi-needs-can", ["UNSAFE=1"], [ - step("build openpilot", "cd system/manager && ./build.py"), - step("check dirty", "release/check-dirty.sh"), - step("onroad tests", "pytest selfdrive/test/test_onroad.py -s", [timeout: 60]), + step("build openpilot", "cd openpilot/system/manager && ./build.py"), + step("check dirty", "tools/release/check-dirty.sh"), + step("onroad tests", "pytest openpilot/selfdrive/test/test_onroad.py -s", [timeout: 60]), ]) }, 'HW + Unit Tests': { deviceStage("tizi-hardware", "tizi-common", ["UNSAFE=1"], [ - step("build", "cd system/manager && ./build.py"), - step("test power draw", "pytest -s system/hardware/tici/tests/test_power_draw.py"), - step("test encoder", "LD_LIBRARY_PATH=/usr/local/lib pytest system/loggerd/tests/test_encoder.py", [diffPaths: ["system/loggerd/"]]), - step("test manager", "pytest system/manager/test/test_manager.py"), + step("build", "cd openpilot/system/manager && ./build.py"), + step("test power draw", "pytest -s openpilot/selfdrive/test//test_power_draw.py"), + step("test encoder", "pytest openpilot/system/loggerd/tests/test_encoder.py", [diffPaths: ["openpilot/system/loggerd/"]]), + step("test manager", "pytest openpilot/system/manager/test/test_manager.py"), ]) }, 'camerad OX03C10': { deviceStage("OX03C10", "tizi-ox03c10", ["UNSAFE=1"], [ - step("build", "cd system/manager && ./build.py"), - step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py"), - step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 90]), + step("build", "cd openpilot/system/manager && ./build.py"), + step("test pandad", "pytest openpilot/selfdrive/pandad/tests/test_pandad.py"), + step("test camerad", "pytest openpilot/system/camerad/test/test_camerad.py", [timeout: 90]), ]) }, 'camerad OS04C10': { deviceStage("OS04C10", "tici-os04c10", ["UNSAFE=1"], [ - step("build", "cd system/manager && ./build.py"), - step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py"), - step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 90]), + step("build", "cd openpilot/system/manager && ./build.py"), + step("test pandad", "pytest openpilot/selfdrive/pandad/tests/test_pandad.py"), + step("test camerad", "pytest openpilot/system/camerad/test/test_camerad.py", [timeout: 90]), ]) }, 'sensord': { deviceStage("LSM + MMC", "tizi-lsmc", ["UNSAFE=1"], [ - step("build", "cd system/manager && ./build.py"), - step("test sensord", "pytest system/sensord/tests/test_sensord.py"), + step("build", "cd openpilot/system/manager && ./build.py"), + step("test sensord", "pytest openpilot/system/sensord/tests/test_sensord.py"), ]) }, 'replay': { deviceStage("model-replay", "tizi-replay", ["UNSAFE=1"], [ - step("build", "cd system/manager && ./build.py", [diffPaths: ["selfdrive/modeld/", "tinygrad_repo", "selfdrive/test/process_replay/model_replay.py"]]), - step("model replay", "selfdrive/test/process_replay/model_replay.py", [diffPaths: ["selfdrive/modeld/", "tinygrad_repo", "selfdrive/test/process_replay/model_replay.py"]]), + step("build", "cd openpilot/system/manager && ./build.py", [diffPaths: ["openpilot/selfdrive/modeld/", "tinygrad_repo", "openpilot/selfdrive/test/process_replay/model_replay.py"]]), + step("model replay", "openpilot/selfdrive/test/process_replay/model_replay.py", [diffPaths: ["openpilot/selfdrive/modeld/", "tinygrad_repo", "openpilot/selfdrive/test/process_replay/model_replay.py"]]), ]) }, 'tizi': { deviceStage("tizi", "tizi", ["UNSAFE=1"], [ - step("build openpilot", "cd system/manager && ./build.py"), - step("test pandad loopback", "pytest selfdrive/pandad/tests/test_pandad_loopback.py"), - step("test pandad spi", "pytest selfdrive/pandad/tests/test_pandad_spi.py"), - step("test amp", "pytest system/hardware/tici/tests/test_amplifier.py"), + step("build openpilot", "cd openpilot/system/manager && ./build.py"), + step("test pandad loopback", "pytest openpilot/selfdrive/pandad/tests/test_pandad_loopback.py"), + step("test pandad spi", "pytest openpilot/selfdrive/pandad/tests/test_pandad_spi.py"), + step("test amp", "pytest openpilot/common/hardware/tici/tests/test_amplifier.py"), ]) }, diff --git a/RELEASES.md b/RELEASES.md index 7946fce019..770a7c40fe 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1104,7 +1104,7 @@ Version 0.2.1 (2016-12-14) Version 0.2 (2016-12-12) ========================= - * Car/Radar abstraction layers have shipped, see cereal/car.capnp + * Car/Radar abstraction layers have shipped, see openpilot/cereal/car.capnp * controlsd has been refactored * Shipped plant model and testing maneuvers * visiond exits more gracefully now diff --git a/SConstruct b/SConstruct index e714571d81..8cf191daab 100644 --- a/SConstruct +++ b/SConstruct @@ -27,6 +27,21 @@ AddOption('--minimal', default=(not TICI and not release), help='the minimum build to run openpilot. no tests, tools, etc.') +submodule_python_paths = [ + Dir("#").abspath, + Dir("#msgq_repo").abspath, + Dir("#opendbc_repo").abspath, + Dir("#rednose_repo").abspath, + Dir("#teleoprtc_repo").abspath, + Dir("#tinygrad_repo").abspath, +] +for p in reversed(submodule_python_paths): + if p not in sys.path: + sys.path.insert(0, p) + +if external_pythonpath := os.environ.get("PYTHONPATH"): + submodule_python_paths += [p for p in external_pythonpath.split(os.pathsep) if p and p not in submodule_python_paths] + # Detect platform arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip() if platform.system() == "Darwin": @@ -40,9 +55,24 @@ assert arch in [ "Darwin", # macOS arm64 (x86 not supported) ] -pkg_names = ['acados', 'bzip2', 'capnproto', 'catch2', 'eigen', 'ffmpeg', 'json11', 'libjpeg', 'libyuv', 'ncurses', 'zeromq', 'zstd'] +pkg_names = ['acados', 'bzip2', 'capnproto', 'catch2', 'eigen', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd'] pkgs = [importlib.import_module(name) for name in pkg_names] acados = pkgs[pkg_names.index('acados')] +ffmpeg = pkgs[pkg_names.index('ffmpeg')] +# Shared package ships .so/.dylib; older device venvs still have static .a only. +# Keep static link deps (x264/z/va/drm) when the installed package is static so +# TICI CI works without upgrading the device venv yet. +# TODO: drop the static fallback once device venvs have comma-deps-ffmpeg>=7.1.0.post94 +_ffmpeg_lib_names = os.listdir(ffmpeg.LIB_DIR) if os.path.isdir(ffmpeg.LIB_DIR) else [] +ffmpeg_shared = any( + n.startswith('libavcodec.so') or (n.startswith('libavcodec') and n.endswith('.dylib')) + for n in _ffmpeg_lib_names +) +ffmpeg_libs = ['avformat', 'avcodec', 'swresample', 'avutil'] +if not ffmpeg_shared: + ffmpeg_libs += ['x264', 'z'] + if arch != "Darwin": + ffmpeg_libs += ['va', 'va-drm', 'drm'] acados_include_dirs = [ acados.INCLUDE_DIR, os.path.join(acados.INCLUDE_DIR, "blasfeo", "include"), @@ -91,7 +121,7 @@ def _libflags(target, source, env, for_signature): env = Environment( ENV={ "PATH": os.environ['PATH'], - "PYTHONPATH": Dir("#").abspath, + "PYTHONPATH": os.pathsep.join(submodule_python_paths), "ACADOS_SOURCE_DIR": acados.DIR, "ACADOS_PYTHON_INTERFACE_PATH": acados.TEMPLATE_DIR, "TERA_PATH": acados.TERA_PATH @@ -112,25 +142,35 @@ env = Environment( CFLAGS=["-std=gnu11"], CXXFLAGS=["-std=c++1z"], CPPPATH=[ - "#", - "#msgq", + "#openpilot", + "#msgq_repo", # #include "msgq/..." + "#opendbc_repo", # #include "opendbc/..." + "#rednose_repo", # #include "rednose/..." + "#rednose_repo/rednose", # #include "logger/..." (rednose package root) + "#openpilot/cereal/gen/cpp", acados_include_dirs, [x.INCLUDE_DIR for x in pkgs], + "#", ], LIBPATH=[ - "#common", + "#openpilot/common", "#msgq_repo", - "#selfdrive/pandad", - "#rednose/helpers", + "#openpilot/selfdrive/pandad", + "#rednose_repo/rednose/helpers", [x.LIB_DIR for x in pkgs], ], - RPATH=[], + RPATH=[ffmpeg.LIB_DIR] if ffmpeg_shared else [], CYTHONCFILESUFFIX=".cpp", COMPILATIONDB_USE_ABSPATH=True, - REDNOSE_ROOT="#", + REDNOSE_ROOT="#rednose_repo", tools=["default", "cython", "compilation_db", "rednose_filter"], toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"], ) +# SCons' Darwin linker tool doesn't define the variables used to expand RPATH. +if arch == "Darwin": + env["RPATHPREFIX"] = "-Wl,-rpath," + env["RPATHSUFFIX"] = "" + env["_RPATH"] = "${_concat(RPATHPREFIX, RPATH, RPATHSUFFIX, __env__)}" if arch != "larch64": env['_LIBFLAGS'] = _libflags @@ -190,7 +230,7 @@ else: np_version = SCons.Script.Value(np.__version__) Export('envCython', 'np_version') -Export('env', 'arch', 'acados', 'release') +Export('env', 'arch', 'acados', 'release', 'ffmpeg_libs') # Setup cache dir default_cache_dir = '/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache' @@ -211,7 +251,7 @@ def prune_cache_dir(target=None, source=None, env=None): # ********** start building stuff ********** # Build common module -SConscript(['common/SConscript']) +SConscript(['openpilot/common/SConscript']) Import('_common') common = [_common, 'json11', 'zmq'] Export('common') @@ -222,7 +262,7 @@ env_swaglog = env.Clone() env_swaglog['CXXFLAGS'].append('-DSWAGLOG="\\"common/swaglog.h\\""') SConscript(['msgq_repo/SConscript'], exports={'env': env_swaglog}) -SConscript(['cereal/SConscript']) +SConscript(['openpilot/cereal/SConscript']) Import('socketmaster', 'msgq') messaging = [socketmaster, msgq, 'capnp', 'kj',] @@ -233,34 +273,33 @@ Export('messaging') SConscript(['panda/SConscript']) # Build rednose library -SConscript(['rednose/SConscript']) +SConscript(['rednose_repo/rednose/SConscript']) # Build system services SConscript([ - 'system/loggerd/SConscript', + 'openpilot/system/loggerd/SConscript', ]) if arch == "larch64": - SConscript(['system/camerad/SConscript']) + SConscript(['openpilot/system/camerad/SConscript']) # Build selfdrive SConscript([ - 'selfdrive/pandad/SConscript', - 'selfdrive/controls/lib/lateral_mpc_lib/SConscript', - 'selfdrive/controls/lib/longitudinal_mpc_lib/SConscript', - 'selfdrive/locationd/SConscript', - 'selfdrive/modeld/SConscript', - 'selfdrive/ui/SConscript', + 'openpilot/selfdrive/pandad/SConscript', + 'openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript', + 'openpilot/selfdrive/locationd/SConscript', + 'openpilot/selfdrive/modeld/SConscript', + 'openpilot/selfdrive/ui/SConscript', ]) -SConscript(['sunnypilot/SConscript']) +SConscript(['openpilot/sunnypilot/SConscript']) # Build desktop-only tools if GetOption('extras') and arch != "larch64": SConscript([ - 'tools/replay/SConscript', - 'tools/cabana/SConscript', - 'tools/jotpluggler/SConscript', + 'openpilot/tools/replay/SConscript', + 'openpilot/tools/cabana/SConscript', + 'openpilot/tools/jotpluggler/SConscript', ]) @@ -276,6 +315,8 @@ def count_scons_nodes(nodes): if node in seen: continue seen.add(node) + if hasattr(node, 'has_builder') and node.has_builder(): + build_product_nodes.add(node) executor = node.get_executor() if executor is not None: stack += executor.get_all_prerequisites() + executor.get_all_children() @@ -284,6 +325,7 @@ def count_scons_nodes(nodes): progress_interval = 5 progress_count = 0 +build_product_nodes = set() progress_total = max(1, count_scons_nodes(env.arg2nodes(BUILD_TARGETS or [Dir('.')], env.fs.Entry))) def progress_function(node): @@ -299,3 +341,11 @@ def progress_function(node): Progress(progress_function, interval=progress_interval) AddPostAction(BUILD_TARGETS or [Dir('.')], prune_cache_dir) + +def check_build_product_size(target, source, env): + limit = 50 * 1024 * 1024 # GitHub max size + for t in target: + if hasattr(t, 'isfile') and t.isfile() and (size := os.path.getsize(t.abspath)) > limit: + raise SCons.Errors.UserError(f"{t} is {size / (1024 * 1024):.1f} MiB, exceeding the {limit / (1024 * 1024):.1f} MiB limit") +if not GetOption('extras'): + AddPostAction(list(build_product_nodes), Action(check_build_product_size, None)) diff --git a/cereal/SConscript b/cereal/SConscript deleted file mode 100644 index de7ca0d721..0000000000 --- a/cereal/SConscript +++ /dev/null @@ -1,20 +0,0 @@ -Import('env', 'common', 'msgq') - -cereal_dir = Dir('.') -gen_dir = Dir('gen') - -# Build cereal -schema_files = ['log.capnp', 'car.capnp', 'deprecated.capnp', 'custom.capnp'] -env.Command([f'gen/cpp/{s}.c++' for s in schema_files] + [f'gen/cpp/{s}.h' for s in schema_files], - schema_files, - f"capnpc --src-prefix={cereal_dir.path} $SOURCES -o c++:{gen_dir.path}/cpp/") - -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', 'messaging/bridge_zmq.cc'], LIBS=[msgq, common, 'pthread']) - -socketmaster = env.Library('socketmaster', ['messaging/socketmaster.cc']) - -Export('cereal', 'socketmaster') diff --git a/cereal/__init__.py b/cereal/__init__.py deleted file mode 100644 index 93f4d77227..0000000000 --- a/cereal/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -import os -import capnp -from importlib.resources import as_file, files - -capnp.remove_import_hook() - -with as_file(files("cereal")) as fspath: - CEREAL_PATH = fspath.as_posix() - log = capnp.load(os.path.join(CEREAL_PATH, "log.capnp")) - car = capnp.load(os.path.join(CEREAL_PATH, "car.capnp")) - custom = capnp.load(os.path.join(CEREAL_PATH, "custom.capnp")) diff --git a/cereal/car.capnp b/cereal/car.capnp deleted file mode 120000 index 4bc7f89b1f..0000000000 --- a/cereal/car.capnp +++ /dev/null @@ -1 +0,0 @@ -../opendbc_repo/opendbc/car/car.capnp \ No newline at end of file diff --git a/cereal/messaging/tests/validate_sp_cereal_upstream.py b/cereal/messaging/tests/validate_sp_cereal_upstream.py deleted file mode 100755 index 11e39cd6ce..0000000000 --- a/cereal/messaging/tests/validate_sp_cereal_upstream.py +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env python3 -"""Schema-level cereal compat check between sunnypilot and upstream openpilot. - -Rules (per struct matched across sides by typeId): - R1 shared ordinal must reference the same type. - R2 sunnypilot-only ordinal in a union -> FAIL (unknown discriminant upstream). - R3 sunnypilot-only ordinal on a regular field -> OK (additive struct evolution). - R4 upstream-only ordinal -> OK. - R5 sunnypilot-only struct referenced via an upstream-shared field -> FAIL. -""" - -from __future__ import annotations - -import argparse -import json -import os -import sys -from typing import Any - -NO_DISCRIMINANT = 0xFFFF - - -def hex_id(value: int) -> str: - return f"0x{value:016x}" - - -def encode_type(type_node: Any) -> dict: - which = type_node.which() - if which == "struct": - return {"kind": "struct", "typeId": hex_id(type_node.struct.typeId)} - if which == "enum": - return {"kind": "enum", "typeId": hex_id(type_node.enum.typeId)} - if which == "interface": - return {"kind": "interface", "typeId": hex_id(type_node.interface.typeId)} - if which == "list": - return {"kind": "list", "element": encode_type(type_node.list.elementType)} - if which == "anyPointer": - return {"kind": "anyPointer"} - return {"kind": which} - - -def encode_field(name: str, field: Any) -> dict: - proto = field.proto - ordinal = proto.ordinal.explicit if proto.ordinal.which() == "explicit" else None - discriminant = proto.discriminantValue if proto.discriminantValue != NO_DISCRIMINANT else None - - if proto.which() == "group": - type_desc = {"kind": "group", "typeId": hex_id(proto.group.typeId)} - else: - type_desc = encode_type(proto.slot.type) - - return { - "name": name, - "ordinal": ordinal, - "discriminant": discriminant, - "type": type_desc, - } - - -def encode_struct(schema: Any) -> dict: - node = schema.node - return { - "typeId": hex_id(node.id), - "displayName": node.displayName, - "hasUnion": node.struct.discriminantCount > 0, - "fields": [encode_field(name, field) for name, field in schema.fields.items()], - } - - -def _child_struct_schema(field: Any) -> Any: - proto = field.proto - if proto.which() == "group": - return field.schema - type_node = proto.slot.type - which = type_node.which() - if which == "struct": - return field.schema - if which == "list": - container = field.schema - element_type = type_node.list.elementType - while element_type.which() == "list": - container = container.elementType - element_type = element_type.list.elementType - if element_type.which() == "struct": - return container.elementType - return None - - -def collect_schema(root: Any) -> dict[str, dict]: - structs: dict[str, dict] = {} - stack = [root] - while stack: - schema = stack.pop() - type_id = hex_id(schema.node.id) - if type_id in structs: - continue - structs[type_id] = encode_struct(schema) - for _name, field in schema.fields.items(): - try: - child = _child_struct_schema(field) - except Exception: - child = None - if child is not None: - stack.append(child) - return structs - - -def load_log(cereal_dir: str) -> Any: - import capnp - cereal_dir = os.path.abspath(cereal_dir) - capnp.remove_import_hook() - return capnp.load(os.path.join(cereal_dir, "log.capnp"), imports=[cereal_dir]) - - -def dump_schema(cereal_dir: str, path: str) -> None: - log = load_log(cereal_dir) - payload = { - "root": hex_id(log.Event.schema.node.id), - "structs": collect_schema(log.Event.schema), - } - with open(path, "w", encoding="utf-8") as handle: - json.dump(payload, handle, indent=2, sort_keys=True) - print(f"wrote schema dump with {len(payload['structs'])} structs to {path}") - - -def types_equal(a: dict, b: dict) -> bool: - if a.get("kind") != b.get("kind"): - return False - kind = a["kind"] - if kind in ("struct", "enum", "interface", "group"): - return a.get("typeId") == b.get("typeId") - if kind == "list": - return types_equal(a["element"], b["element"]) - return True - - -def type_repr(t: dict) -> str: - kind = t.get("kind", "?") - if kind in ("struct", "enum", "interface", "group"): - return f"{kind}({t.get('typeId')})" - if kind == "list": - return f"list<{type_repr(t['element'])}>" - return kind - - -def field_is_union_variant(field: dict) -> bool: - return field.get("discriminant") is not None - - -def index_fields_by_ordinal(struct: dict) -> dict[int, dict]: - indexed: dict[int, dict] = {} - for field in struct["fields"]: - ordinal = field.get("ordinal") - if ordinal is None: - continue - indexed[ordinal] = field - return indexed - - -def compare(sunnypilot_dump: dict, upstream_dump: dict) -> list[str]: - violations: list[str] = [] - sunnypilot_structs: dict[str, dict] = sunnypilot_dump["structs"] - upstream_structs: dict[str, dict] = upstream_dump["structs"] - - sunnypilot_struct_referenced_from_shared: set[str] = set() - - for type_id, sunnypilot_struct in sunnypilot_structs.items(): - upstream_struct = upstream_structs.get(type_id) - if upstream_struct is None: - continue - - sunnypilot_fields = index_fields_by_ordinal(sunnypilot_struct) - upstream_fields = index_fields_by_ordinal(upstream_struct) - display = sunnypilot_struct["displayName"] - - for ordinal, sunnypilot_field in sunnypilot_fields.items(): - upstream_field = upstream_fields.get(ordinal) - if upstream_field is None: - if field_is_union_variant(sunnypilot_field): - violations.append( - f"[R2] {display} @{ordinal} ('{sunnypilot_field['name']}', {type_repr(sunnypilot_field['type'])}): " - f"union variant not present upstream. upstream cannot parse this discriminant." - ) - continue - - if not types_equal(sunnypilot_field["type"], upstream_field["type"]): - violations.append( - f"[R1] {display} @{ordinal}: type mismatch. " - f"sunnypilot='{sunnypilot_field['name']}' {type_repr(sunnypilot_field['type'])} vs " - f"upstream='{upstream_field['name']}' {type_repr(upstream_field['type'])}." - ) - continue - - cursor = sunnypilot_field["type"] - while cursor.get("kind") == "list": - cursor = cursor["element"] - if cursor.get("kind") in ("struct", "group", "interface") and cursor.get("typeId"): - sunnypilot_struct_referenced_from_shared.add(cursor["typeId"]) - - for type_id, sunnypilot_struct in sunnypilot_structs.items(): - if type_id in upstream_structs: - continue - if type_id in sunnypilot_struct_referenced_from_shared: - violations.append( - f"[R5] struct {sunnypilot_struct['displayName']} ({type_id}) exists only on sunnypilot " - f"but is referenced from an upstream-shared field. upstream cannot resolve this type." - ) - - return violations - - -def load_peer(path: str) -> dict: - with open(path, "r", encoding="utf-8") as handle: - return json.load(handle) - - -def run_read(cereal_dir: str, peer_path: str) -> int: - log = load_log(cereal_dir) - peer_dump = load_peer(peer_path) - local_dump = { - "root": hex_id(log.Event.schema.node.id), - "structs": collect_schema(log.Event.schema), - } - violations = compare(sunnypilot_dump=peer_dump, upstream_dump=local_dump) - - if not violations: - print("cereal compat OK: upstream openpilot can parse sunnypilot routes " - "(no leaked structs, no ordinal collisions).") - return 0 - - print(f"cereal compat FAIL: upstream openpilot would misparse sunnypilot routes " - f"({len(violations)} violation(s)):") - for v in violations: - print(f" {v}") - return 1 - - -def main() -> int: - parser = argparse.ArgumentParser( - description="sunnypilot <-> upstream cereal compatibility validator (schema-level)." - ) - mode = parser.add_mutually_exclusive_group(required=True) - mode.add_argument("-g", "--generate", action="store_true", help="dump local schema to JSON") - mode.add_argument("-r", "--read", action="store_true", help="load peer JSON and diff against local") - parser.add_argument("-f", "--file", default="schema.json", help="JSON file path (default: schema.json)") - parser.add_argument("--cereal-dir", required=True, help="path to cereal directory containing log.capnp") - args = parser.parse_args() - - if args.generate: - dump_schema(args.cereal_dir, args.file) - return 0 - return run_read(args.cereal_dir, args.file) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/common/tests/test_params.cc b/common/tests/test_params.cc deleted file mode 100644 index f8d6c79f55..0000000000 --- a/common/tests/test_params.cc +++ /dev/null @@ -1,27 +0,0 @@ -#include "catch2/catch.hpp" -#define private public -#include "common/params.h" -#include "common/util.h" - -TEST_CASE("params_nonblocking_put") { - char tmp_path[] = "/tmp/asyncWriter_XXXXXX"; - const std::string param_path = mkdtemp(tmp_path); - auto param_names = {"CarParams", "IsMetric"}; - { - Params params(param_path); - for (const auto &name : param_names) { - params.putNonBlocking(name, "1"); - // param is empty - REQUIRE(params.get(name).empty()); - } - - // check if thread is running - REQUIRE(params.future.valid()); - REQUIRE(params.future.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout); - } - // check results - Params p(param_path); - for (const auto &name : param_names) { - REQUIRE(p.get(name) == "1"); - } -} diff --git a/conftest.py b/conftest.py index de37572317..d7bc7a6bf1 100644 --- a/conftest.py +++ b/conftest.py @@ -5,21 +5,27 @@ import pytest from openpilot.common.prefix import OpenpilotPrefix from openpilot.system.manager import manager -from openpilot.system.hardware import TICI, HARDWARE +from openpilot.common.hardware import TICI, HARDWARE # these are heavy CI-only tests, invoked explicitly in .github/workflows/tests.yaml collect_ignore = [ - "selfdrive/test/process_replay/test_processes.py", - "selfdrive/test/process_replay/test_regen.py", + "openpilot/selfdrive/test/process_replay/test_processes.py", + "openpilot/selfdrive/test/process_replay/test_regen.py", + + "openpilot/tools/sim/", + # tinygrad JIT has process-global state. Other test files import modeld → tinygrad, # which corrupts JIT captures for test_warp.py in the same process. Run separately in CI. - "sunnypilot/modeld_v2/tests/test_warp.py", -] -collect_ignore_glob = [ - "selfdrive/debug/*.py", + "openpilot/sunnypilot/modeld_v2/tests/test_warp.py", ] +def pytest_sessionstart(session): + # TODO: fix tests and enable test order randomization + if session.config.pluginmanager.hasplugin('randomly'): + session.config.option.randomly_reorganize = False + + @pytest.hookimpl(hookwrapper=True, trylast=True) def pytest_runtest_call(item): # ensure we run as a hook after capturemanager's diff --git a/docs/CARS.md b/docs/CARS.md index 288d5b7a47..c52707f4f3 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,24 +4,24 @@ 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. -# 340 Supported Cars +# 341 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 2022-24|All|openpilot available[1](#footnotes)|0 mph|43 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
||| +|Acura|MDX 2022-24|All|openpilot available[1,5](#footnotes)|0 mph|43 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
||| |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-22|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
||| +|Acura|RDX 2019-21|All|openpilot available[1,5](#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-22|All|openpilot available[1,5](#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
||| |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[11](#footnotes)|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[12](#footnotes)|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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|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
||| @@ -33,7 +33,7 @@ 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[11](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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
||| +|CUPRA[12](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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
||| @@ -47,8 +47,8 @@ A supported vehicle is one that just works when you install a comma device. All |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 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|Focus 2018-22[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-22[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 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| @@ -75,35 +75,35 @@ A supported vehicle is one that just works when you install a comma device. All |Genesis|GV70 Electrified (with HDA II) 2023-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 Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Genesis|GV80 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| -|Honda|Accord 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Accord 2018-22|All|openpilot available[1,5](#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|Accord 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|Accord Hybrid 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Accord Hybrid 2018-22|All|openpilot available[1,5](#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|Accord 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|City (Brazil only) 2023|All|openpilot available[1](#footnotes)|0 mph|14 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|City (Brazil only) 2023|All|openpilot available[1,5](#footnotes)|0 mph|14 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 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[4](#footnotes)|[![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|Civic 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Honda|Civic Hatchback 2017-18|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|Civic Hatchback 2019-21|All|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|Civic Hatchback 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|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|Civic 2019-21|All|openpilot available[1,5](#footnotes)|0 mph|2 mph[4](#footnotes)|[![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|Civic 2022-24|All|openpilot available[1,5](#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 2017-18|Honda Sensing|openpilot available[1,5](#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|Civic Hatchback 2019-21|All|openpilot available[1,5](#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|Civic Hatchback 2022-24|All|openpilot available[1,5](#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 2025-26|All|openpilot available[1,5](#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,5](#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,5](#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|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 2017-22|Honda Sensing|openpilot available[1,5](#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 2017-22|Honda Sensing|openpilot available[1,5](#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-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|e 2020|All|openpilot available[1,5](#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
||| |Honda|HR-V 2019-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Honda|HR-V 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Honda|Insight 2019-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Honda|Inspire 2018|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|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|HR-V 2023-25|All|openpilot available[1,5](#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|Insight 2019-22|All|openpilot available[1,5](#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|Inspire 2018|All|openpilot available[1,5](#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|N-Box 2018|All|openpilot available[1,5](#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 2021-26|All|openpilot available[1,5](#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 (Singapore) 2021|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|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
||| @@ -126,6 +126,7 @@ A supported vehicle is one that just works when you install a comma device. All |Hyundai|Ioniq 5 (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 Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Ioniq 5 (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 K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Ioniq 6 (with HDA II) 2023-24|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Hyundai|Ioniq 6 (without HDA II) 2023-24|Highway Driving Assist|Stock|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
||| |Hyundai|Ioniq Electric 2019|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Ioniq Electric 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Ioniq Hybrid 2017-19|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -223,14 +224,14 @@ 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[11](#footnotes)|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[12](#footnotes)|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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 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
||| +|Nissan[6](#footnotes)|Altima 2019-24|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[6](#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[6](#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[6](#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
||| @@ -238,35 +239,35 @@ A supported vehicle is one that just works when you install a comma device. All |Rivian|R1S 2025|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 B 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
||| |Rivian|R1T 2025|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 B connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|SEAT[11](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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)
||| -|Subaru|Forester 2017-18|EyeSight Driver Assistance[6](#footnotes)|Stock|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|Forester 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|Impreza 2017-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|Impreza 2020-22|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)
||| -|Subaru|Legacy 2015-18|EyeSight Driver Assistance[6](#footnotes)|Stock|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|Legacy 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|Outback 2015-17|EyeSight Driver Assistance[6](#footnotes)|Stock|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|Outback 2018-19|EyeSight Driver Assistance[6](#footnotes)|Stock|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|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[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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
[16](#footnotes)||| -|Škoda|Kamiq 2021-23[12,14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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
[16](#footnotes)||| -|Škoda[11](#footnotes)|Karoq 2019-23[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Kodiaq 2017-23[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Octavia 2015-19[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Octavia RS 2016[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Octavia Scout 2017-19[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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
[16](#footnotes)||| -|Škoda[11](#footnotes)|Superb 2015-22[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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
||| +|SEAT[12](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[7](#footnotes)|openpilot available[1,8](#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[7](#footnotes)|openpilot available[1,8](#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[7](#footnotes)|openpilot available[1,8](#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|Forester 2017-18|EyeSight Driver Assistance[7](#footnotes)|Stock|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|Forester 2019-21|All[7](#footnotes)|openpilot available[1,8](#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|Impreza 2017-19|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#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|Impreza 2020-22|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#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|Legacy 2015-18|EyeSight Driver Assistance[7](#footnotes)|Stock|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|Legacy 2020-22|All[7](#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|Outback 2015-17|EyeSight Driver Assistance[7](#footnotes)|Stock|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|Outback 2018-19|EyeSight Driver Assistance[7](#footnotes)|Stock|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|Outback 2020-22|All[7](#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[7](#footnotes)|openpilot available[1,8](#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[7](#footnotes)|openpilot available[1,8](#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[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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
[17](#footnotes)||| +|Škoda|Kamiq 2021-23[13,15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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
[17](#footnotes)||| +|Škoda[12](#footnotes)|Karoq 2019-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Kodiaq 2017-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Octavia 2015-19[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Octavia RS 2016[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Octavia Scout 2017-19[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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
[17](#footnotes)||| +|Škoda[12](#footnotes)|Superb 2015-22[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[10](#footnotes)|Model 3 (with HW3) 2019-23[9](#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[10](#footnotes)|Model 3 (with HW4) 2024-25[9](#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[10](#footnotes)|Model Y (with HW3) 2020-23[9](#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[10](#footnotes)|Model Y (with HW4) 2024-25[9](#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
||| @@ -279,8 +280,8 @@ A supported vehicle is one that just works when you install a comma device. All |Toyota|C-HR 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|C-HR Hybrid 2021-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Camry 2018-20|All|Stock|0 mph[10](#footnotes)|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
||| -|Toyota|Camry 2021-24|All|openpilot|0 mph[10](#footnotes)|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|Camry 2018-20|All|Stock|0 mph[11](#footnotes)|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
||| +|Toyota|Camry 2021-24|All|openpilot|0 mph[11](#footnotes)|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|Camry Hybrid 2018-20|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
||| |Toyota|Camry Hybrid 2021-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Corolla 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
||| @@ -312,60 +313,61 @@ 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[11](#footnotes)|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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,15](#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
[16](#footnotes)||| -|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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
[16](#footnotes)||| -|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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
[16](#footnotes)||| -|Volkswagen[11](#footnotes)|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[11](#footnotes)|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#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[12](#footnotes)|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 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[12](#footnotes)|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 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[12](#footnotes)|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 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[12](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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,16](#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
[17](#footnotes)||| +|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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
[17](#footnotes)||| +|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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
[17](#footnotes)||| +|Volkswagen[12](#footnotes)|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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[12](#footnotes)|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 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 `nightly-dev`.
2Refers only to the Focus Mk4 (C519) available in Europe/China/Taiwan/Australasia, not the Focus Mk3 (C346) in North and South America/Southeast Asia.
3See more setup details for GM.
42019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph.
-5See more setup details for Nissan.
-6In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance.
-7Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB.
-8Some 2023 model years have HW4. To check which hardware type your vehicle has, look for Autopilot computer under Software -> Additional Vehicle Information on your vehicle's touchscreen. See this page for more information.
-9See more setup details for Tesla.
-10openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.
-11The J533 harness plugs in at the CAN gateway under the dashboard, just above the steering column. More information can be found at this guide.
-12Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
-13Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets.
-14Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality.
-15Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC.
-16Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot in software, but doesn't yet have a harness available from the comma store.
+5Enabling longitudinal control (alpha) will disable all CMBS functionality, including AEB and FCW.
+6See more setup details for Nissan.
+7In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance.
+8Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB.
+9Some 2023 model years have HW4. To check which hardware type your vehicle has, look for Autopilot computer under Software -> Additional Vehicle Information on your vehicle's touchscreen. See this page for more information.
+10See more setup details for Tesla.
+11openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.
+12The J533 harness plugs in at the CAN gateway under the dashboard, just above the steering column. More information can be found at this guide.
+13Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
+14Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets.
+15Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality.
+16Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC.
+17Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot in software, but doesn't yet have a harness available from the comma store.
## Community Maintained Cars Although they're not upstream, the community has openpilot running on other makes and models. See the 'Community Supported Models' section of each make [on our wiki](https://wiki.comma.ai/). diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 3d39420c01..cbeb5f6d3a 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -68,6 +68,6 @@ A good pull request has all of the following: ### A guide for forks In order for your fork's data to be eligible for the training set: -* **Your cereal messaging structs must be [compatible](../cereal#custom-forks)** +* **Your cereal messaging structs must be [compatible](../openpilot/cereal#custom-forks)** * **The definitions of all the stock messaging structs must not change**: Do not change how any of the fields are set, including everything from `selfdriveState.enabled` to `carState.steeringAngleDeg`. Instead, create your own structs and set them however you'd like. * **Do not include cars that are not supported in upstream platforms**: Instead, create new opendbc platforms for cars that you'd like to support outside of upstream, even if it's just a trim-level difference. diff --git a/docs/SAFETY.md b/docs/SAFETY.md index 0a662ac6cd..5857c6d5b5 100644 --- a/docs/SAFETY.md +++ b/docs/SAFETY.md @@ -35,8 +35,8 @@ For additional safety implementation details, refer to [panda safety model](http ### Forks of openpilot -* Do not disable or nerf [driver monitoring](https://github.com/commaai/openpilot/tree/master/selfdrive/monitoring) -* Do not disable or nerf [excessive actuation checks](https://github.com/commaai/openpilot/tree/master/selfdrive/selfdrived/helpers.py) +* Do not disable or nerf [driver monitoring](https://github.com/commaai/openpilot/tree/master/openpilot/selfdrive/monitoring) +* Do not disable or nerf [excessive actuation checks](https://github.com/commaai/openpilot/tree/master/openpilot/selfdrive/selfdrived/helpers.py) * If your fork modifies any of the code in `opendbc/safety/`: * your fork cannot use the openpilot trademark * your fork must preserve the full [safety test suite](https://github.com/commaai/opendbc/tree/master/opendbc/safety/tests) and all tests must pass, including any new coverage required by the fork's changes diff --git a/docs/concepts/logs.md b/docs/concepts/logs.md index e533d36297..dd954d4c76 100644 --- a/docs/concepts/logs.md +++ b/docs/concepts/logs.md @@ -2,13 +2,13 @@ openpilot records routes in one minute chunks called segments. A route starts on the rising edge of ignition and ends on the falling edge. -Check out our [Python library](https://github.com/commaai/openpilot/blob/master/tools/lib/logreader.py) for reading openpilot logs. Also checkout our [tools](https://github.com/commaai/openpilot/tree/master/tools) to replay and view your data. These are the same tools we use to debug and develop openpilot. +Check out our [Python library](https://github.com/commaai/openpilot/blob/master/openpilot/tools/lib/logreader.py) for reading openpilot logs. Also checkout our [tools](https://github.com/commaai/openpilot/tree/master/openpilot/tools) to replay and view your data. These are the same tools we use to debug and develop openpilot. For each segment, openpilot records the following log types: ## rlog.zst -rlogs contain all the messages passed amongst openpilot's processes. See [cereal/services.py](https://github.com/commaai/openpilot/blob/master/cereal/services.py) for a list of all the logged services. They're a zstd archive of the serialized [Cap’n Proto](https://capnproto.org/) messages. +rlogs contain all the messages passed amongst openpilot's processes. See [openpilot/cereal/services.py](https://github.com/commaai/openpilot/blob/master/openpilot/cereal/services.py) for a list of all the logged services. They're a zstd archive of the serialized [Cap’n Proto](https://capnproto.org/) messages. ## {f,e,d}camera.hevc @@ -20,7 +20,7 @@ Each camera stream is H.265 encoded and written to its respective file. ## qlog.zst & qcamera.ts -qlogs are a decimated subset of the rlogs. Check out [cereal/services.py](https://github.com/commaai/cereal/blob/master/services.py) for the decimation. +qlogs are a decimated subset of the rlogs. Check out [openpilot/cereal/services.py](https://github.com/commaai/openpilot/blob/master/openpilot/cereal/services.py) for the decimation. qcameras are H.264 encoded, lower res versions of the fcamera.hevc. The video shown in [comma connect](https://connect.comma.ai/) is from the qcameras. diff --git a/docs/how-to/car-port.md b/docs/how-to/car-port.md index ca565e53f6..c4106f91a7 100644 --- a/docs/how-to/car-port.md +++ b/docs/how-to/car-port.md @@ -30,7 +30,7 @@ Each car brand is supported by a standard interface structure in `opendbc/car/[b For historical reasons, openpilot still contains a small amount of car-specific logic. This will eventually be migrated to opendbc or otherwise removed. -* `selfdrive/car/car_specific.py`: Brand-specific event logic +* `openpilot/selfdrive/car/car_specific.py`: Brand-specific event logic # How do I port car? diff --git a/docs/how-to/replay-a-drive.md b/docs/how-to/replay-a-drive.md index a11b29dcc4..129e7b8301 100644 --- a/docs/how-to/replay-a-drive.md +++ b/docs/how-to/replay-a-drive.md @@ -5,7 +5,7 @@ Replaying is a critical tool for openpilot development and debugging. ## Replaying a route *Hardware required: none* -Just run `tools/replay/replay --demo`. +Just run `openpilot/tools/replay/replay --demo`. ## Replaying CAN data *Hardware required: jungle and comma four* diff --git a/docs/how-to/turn-the-speed-blue.md b/docs/how-to/turn-the-speed-blue.md index b5692daff7..5cfa8e176a 100644 --- a/docs/how-to/turn-the-speed-blue.md +++ b/docs/how-to/turn-the-speed-blue.md @@ -28,10 +28,10 @@ scons We'll run the `replay` tool with the demo route to get data streaming for testing our UI changes. ```bash # in terminal 1 -tools/replay/replay --demo +openpilot/tools/replay/replay --demo # in terminal 2 -./selfdrive/ui/ui.py +./openpilot/selfdrive/ui/ui.py ``` The openpilot UI should launch and show a replay of the demo route. @@ -45,10 +45,10 @@ Now let’s update the speed display color in the UI. Search for the function responsible for rendering the current speed: ```bash -git grep "_draw_current_speed" selfdrive/ui/onroad/hud_renderer.py +git grep "_draw_current_speed" openpilot/selfdrive/ui/onroad/hud_renderer.py ``` -You'll find the relevant code inside `selfdrive/ui/onroad/hud_renderer.py`, in this function: +You'll find the relevant code inside `openpilot/selfdrive/ui/onroad/hud_renderer.py`, in this function: ```python def _draw_current_speed(self, rect: rl.Rectangle) -> None: @@ -72,7 +72,7 @@ Change `COLORS.white` to make it **blue** instead of white. A nice soft blue is After making changes, re-run the UI to see your new UI: ```bash -./selfdrive/ui/ui.py +./openpilot/selfdrive/ui/ui.py ``` ![](https://blog.comma.ai/img/blue_speed_ui.png) diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 5e7b4fa0db..4f767f3b19 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -19,12 +19,12 @@ function agnos_init { # Check if AGNOS update is required if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then - AGNOS_PY="$DIR/system/hardware/tici/agnos.py" - MANIFEST="$DIR/system/hardware/tici/agnos.json" + AGNOS_PY="$DIR/openpilot/common/hardware/tici/agnos.py" + MANIFEST="$DIR/openpilot/system/hardware/tici/agnos.json" if $AGNOS_PY --verify $MANIFEST; then sudo reboot fi - $DIR/system/hardware/tici/updater $AGNOS_PY $MANIFEST + $DIR/openpilot/common/hardware/tici/updater $AGNOS_PY $MANIFEST fi } @@ -70,6 +70,14 @@ function launch { ln -sfn $(pwd) /data/pythonpath export PYTHONPATH="$PWD" + # submodule package symlinks for PYTHONPATH imports on device. + # on PC these come from editable installs via pyproject.toml / uv. + ln -sfn msgq_repo/msgq msgq + ln -sfn opendbc_repo/opendbc opendbc + ln -sfn rednose_repo/rednose rednose + ln -sfn teleoprtc_repo/teleoprtc teleoprtc + ln -sfn tinygrad_repo/tinygrad tinygrad + # hardware specific init if [ -f /AGNOS ]; then agnos_init @@ -79,7 +87,7 @@ function launch { tmux capture-pane -pq -S-1000 > /tmp/launch_log # start manager - cd system/manager + cd openpilot/system/manager if [ ! -f $DIR/prebuilt ]; then ./build.py fi diff --git a/launch_env.sh b/launch_env.sh index d71a5c2767..3e0ca602b4 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1 export QCOM_PRIORITY=12 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="18.4" + export AGNOS_VERSION="18.5" fi export STAGING_ROOT="/data/safe_staging" diff --git a/launch_openpilot.sh b/launch_openpilot.sh index d4841b601f..d6e3424c34 100755 --- a/launch_openpilot.sh +++ b/launch_openpilot.sh @@ -1,20 +1,3 @@ #!/usr/bin/env bash -set -euo pipefail -IFS=$'\n\t' - -# On any failure, run the fallback launcher -trap 'exec ./launch_chffrplus.sh' ERR -C3_LAUNCH_SH="./sunnypilot/system/hardware/c3/launch_chffrplus.sh" - -MODEL="$(tr -d '\0' < "/sys/firmware/devicetree/base/model")" -export MODEL - -if [ "$MODEL" = "comma tici" ]; then - # Force a failure if the launcher doesn't exist - [ -x "$C3_LAUNCH_SH" ] || false - - # If it exists, run it - exec "$C3_LAUNCH_SH" -fi exec ./launch_chffrplus.sh diff --git a/msgq b/msgq deleted file mode 120000 index df09146f62..0000000000 --- a/msgq +++ /dev/null @@ -1 +0,0 @@ -msgq_repo/msgq \ No newline at end of file diff --git a/msgq_repo b/msgq_repo index 9beb84af67..bb6cc57ef4 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit 9beb84af67527f7b6bfee349dcbe3dbb8d4f4789 +Subproject commit bb6cc57ef4d9a3f9952d9ca84bfa580575e7e2a0 diff --git a/opendbc b/opendbc deleted file mode 120000 index 7cd9a5bd1e..0000000000 --- a/opendbc +++ /dev/null @@ -1 +0,0 @@ -opendbc_repo/opendbc \ No newline at end of file diff --git a/opendbc_repo b/opendbc_repo index 10e654bf21..d6b9c1adaa 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 10e654bf216233e72660c7906574d66ed75b1a16 +Subproject commit d6b9c1adaafe2b752c9ee9a4af0024d22c2c12e6 diff --git a/cereal/README.md b/openpilot/cereal/README.md similarity index 86% rename from cereal/README.md rename to openpilot/cereal/README.md index 45e859c09c..17f27fd742 100644 --- a/cereal/README.md +++ b/openpilot/cereal/README.md @@ -24,17 +24,17 @@ things are not. Read more details [here](https://capnproto.org/language.html). ### Custom forks Forks of [openpilot](https://github.com/commaai/openpilot) might want to add things to the messaging -spec, however this could conflict with future changes made in mainline cereal/openpilot. Rebasing against mainline openpilot +spec, however this could conflict with future changes made in mainline openpilot's cereal spec. Rebasing against mainline openpilot then means breaking backwards-compatibility with all old logs of your fork. So we added reserved events in -[custom.capnp](custom.capnp) that we will leave empty in mainline cereal/openpilot. **If you only modify those, you can ensure your +[custom.capnp](custom.capnp) that we will leave empty in mainline openpilot's cereal spec. **If you only modify those, you can ensure your fork will remain backwards-compatible with all versions of mainline openpilot and your fork.** An example of compatible changes: ```diff -diff --git a/cereal/custom.capnp b/cereal/custom.capnp +diff --git a/openpilot/cereal/custom.capnp b/openpilot/cereal/custom.capnp index 3348e859e..3365c7b98 100644 ---- a/cereal/custom.capnp -+++ b/cereal/custom.capnp +--- a/openpilot/cereal/custom.capnp ++++ b/openpilot/cereal/custom.capnp @@ -10,7 +10,11 @@ $Cxx.namespace("cereal"); # DO rename the structs # DON'T change the identifier (e.g. @0x81c2f05a394cf4af) @@ -48,10 +48,10 @@ index 3348e859e..3365c7b98 100644 } struct CustomReserved1 @0xaedffd8f31e7b55d { -diff --git a/cereal/log.capnp b/cereal/log.capnp +diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index 1209f3fd9..b189f58b6 100644 ---- a/cereal/log.capnp -+++ b/cereal/log.capnp +--- a/openpilot/cereal/log.capnp ++++ b/openpilot/cereal/log.capnp @@ -2558,14 +2558,14 @@ struct Event { # DO change the name of the field @@ -76,7 +76,7 @@ index 1209f3fd9..b189f58b6 100644 Example --- ```python -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging # in subscriber sm = messaging.SubMaster(['sensorEvents']) diff --git a/openpilot/cereal/SConscript b/openpilot/cereal/SConscript new file mode 100644 index 0000000000..bee7620b1e --- /dev/null +++ b/openpilot/cereal/SConscript @@ -0,0 +1,22 @@ +Import('env', 'common', 'msgq') + +cereal_dir = Dir('.') +gen_dir = Dir('gen') + +# Build cereal +schema_files = ['log.capnp', 'deprecated.capnp', 'custom.capnp'] +car_capnp = '#opendbc_repo/opendbc/car/car.capnp' +all_output = schema_files + ['car.capnp'] +env.Command([f'gen/cpp/{s}.c++' for s in all_output] + [f'gen/cpp/{s}.h' for s in all_output], + schema_files + [car_capnp], + f"capnpc --src-prefix={cereal_dir.path} --src-prefix=opendbc_repo/opendbc/car --import-path=opendbc_repo/opendbc/car $SOURCES -o c++:{gen_dir.path}/cpp/") + +cereal = env.Library('cereal', [f'gen/cpp/{s}.c++' for s in all_output]) + +# 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', 'messaging/bridge_zmq.cc'], LIBS=[msgq, common, 'pthread']) + +socketmaster = env.Library('socketmaster', ['messaging/socketmaster.cc']) + +Export('cereal', 'socketmaster') diff --git a/openpilot/cereal/__init__.py b/openpilot/cereal/__init__.py new file mode 100644 index 0000000000..d4a43a8474 --- /dev/null +++ b/openpilot/cereal/__init__.py @@ -0,0 +1,11 @@ +import os +import capnp +from importlib.resources import as_file, files + +capnp.remove_import_hook() + +with as_file(files("openpilot.cereal")) as fspath, as_file(files("opendbc")) as opendbc_path: + CEREAL_PATH = fspath.as_posix() + opendbc_import_path = os.path.join(os.path.realpath(opendbc_path.as_posix()), 'car') + log = capnp.load(os.path.join(CEREAL_PATH, "log.capnp"), imports=[opendbc_import_path]) + custom = capnp.load(os.path.join(CEREAL_PATH, "custom.capnp"), imports=[opendbc_import_path]) diff --git a/cereal/custom.capnp b/openpilot/cereal/custom.capnp similarity index 99% rename from cereal/custom.capnp rename to openpilot/cereal/custom.capnp index df8b420568..245c6db00b 100644 --- a/cereal/custom.capnp +++ b/openpilot/cereal/custom.capnp @@ -1,4 +1,4 @@ -using Cxx = import "./include/c++.capnp"; +using Cxx = import "/include/c++.capnp"; $Cxx.namespace("cereal"); @0xb526ba661d550a59; diff --git a/cereal/deprecated.capnp b/openpilot/cereal/deprecated.capnp similarity index 98% rename from cereal/deprecated.capnp rename to openpilot/cereal/deprecated.capnp index 45ce25c682..37153e4d62 100644 --- a/cereal/deprecated.capnp +++ b/openpilot/cereal/deprecated.capnp @@ -1,4 +1,4 @@ -using Cxx = import "./include/c++.capnp"; +using Cxx = import "/include/c++.capnp"; $Cxx.namespace("cereal"); @0x80ef1ec4889c2a63; @@ -760,18 +760,6 @@ struct LateralLQRState @0x9024e2d790c82ade { steeringAngleDesiredDeg @6 :Float32; } -struct LateralCurvatureState @0xad9d8095c06f7c61 { - active @0 :Bool; - actualCurvature @1 :Float32; - desiredCurvature @2 :Float32; - error @3 :Float32; - p @4 :Float32; - i @5 :Float32; - f @6 :Float32; - output @7 :Float32; - saturated @8 :Bool; -} - struct LateralPlannerSolution @0x84caeca5a6b4acfe { x @0 :List(Float32); y @1 :List(Float32); diff --git a/cereal/include/c++.capnp b/openpilot/cereal/include/c++.capnp similarity index 100% rename from cereal/include/c++.capnp rename to openpilot/cereal/include/c++.capnp diff --git a/cereal/log.capnp b/openpilot/cereal/log.capnp similarity index 96% rename from cereal/log.capnp rename to openpilot/cereal/log.capnp index d2c7e2c896..1eaaa3098b 100644 --- a/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -1,7 +1,7 @@ -using Cxx = import "./include/c++.capnp"; +using Cxx = import "/include/c++.capnp"; $Cxx.namespace("cereal"); -using Car = import "car.capnp"; +using Car = import "/car.capnp"; using Deprecated = import "deprecated.capnp"; using Custom = import "custom.capnp"; @@ -414,6 +414,10 @@ struct CanData { struct DeviceState @0xa4d8b5af2aa492eb { deviceType @45 :InitData.DeviceType; + # usb + chestnutPresent @51 :Bool; + usbState @52 :UsbState; + networkType @22 :NetworkType; networkInfo @31 :NetworkInfo; networkStrength @24 :NetworkStrength; @@ -684,33 +688,47 @@ struct PeripheralState { } } +struct UsbState { + devices @0 :List(Device); + + struct Device { + busnum @0 :UInt8; + devnum @1 :UInt8; + vendorId @2 :UInt16; + productId @3 :UInt16; + speedMbps @4 :UInt16; + manufacturer @6 :Text; + product @5 :Text; + linkErrorCount @7 :UInt16; + } +} + struct RadarState @0x9a185389d6fdd05f { - mdMonoTime @6 :UInt64; - carStateMonoTime @11 :UInt64; + mdMonoTime @6 :UInt64; # for debugging radarErrors @13 :Car.RadarData.Error; leadOne @3 :LeadData; leadTwo @4 :LeadData; struct LeadData { - dRel @0 :Float32; - yRel @1 :Float32; - vRel @2 :Float32; - aRel @3 :Float32; - vLead @4 :Float32; - dPath @6 :Float32; - vLat @7 :Float32; - vLeadK @8 :Float32; - aLeadK @9 :Float32; - fcw @10 :Bool; - status @11 :Bool; - aLeadTau @12 :Float32; - modelProb @13 :Float32; - radar @14 :Bool; - radarTrackId @15 :Int32 = -1; + dRel @0 :Float32; # m from the front bumper of the car + yRel @1 :Float32; # m in car frame, left positive + vRel @2 :Float32; # m/s relative longitudinal speed + vLead @4 :Float32; # m/s absolute lead speed + vLeadK @8 :Float32; # kalman-filtered lead speed + aLeadK @9 :Float32; # kalman-filtered lead accel + present @11 :Bool; # true if a lead is present + aLeadTau @12 :Float32; # lead accel time constant + modelProb @13 :Float32; # vision model lead probability + radar @14 :Bool; # true if lead is radar-matched (vs vision-only) + radarTrackId @15 :Int32 = -1; # for debugging deprecated :group { + aRel @3 :Float32; aLead @5 :Float32; + dPath @6 :Float32; + vLat @7 :Float32; + fcw @10 :Bool; } } @@ -723,6 +741,7 @@ struct RadarState @0x9a185389d6fdd05f { calPerc @9 :Int8; canMonoTimes @10 :List(UInt64); cumLagMs @5 :Float32; + carStateMonoTime @11 :UInt64; radarErrors @12 :List(Car.RadarData.ErrorDEPRECATED); } } @@ -771,13 +790,31 @@ struct SelfdriveState { alertStatus @5 :AlertStatus; alertSize @6 :AlertSize; alertType @7 :Text; - alertSound @8 :Car.CarControl.HUDControl.AudibleAlert; + alertSound @13 :AudibleAlert; alertHudVisual @12 :Car.CarControl.HUDControl.VisualAlert; # configurable driving settings experimentalMode @10 :Bool; personality @11 :LongitudinalPersonality; + enum AudibleAlert { + none @0; + + engage @1; + disengage @2; + refuse @3; + + warningSoft @4; + warningImmediate @5; + + prompt @6; + promptRepeat @7; + promptDistracted @8; + + preAlert @9; + complete @10; + } + enum OpenpilotState @0xdbe58b96d2d1ac61 { disabled @0; preEnabled @1; @@ -798,6 +835,10 @@ struct SelfdriveState { mid @2; full @3; } + + deprecated :group { + alertSound @8 :Car.CarControl.HUDControl.AudibleAlert; + } } struct ControlsState @0x97ff69c53601abf1 { @@ -818,7 +859,7 @@ struct ControlsState @0x97ff69c53601abf1 { debugState @59 :LateralDebugState; torqueState @60 :LateralTorqueState; - curvatureStateDEPRECATED @65 :Deprecated.LateralCurvatureState; + curvatureState @65 :LateralCurvatureState; lqrStateDEPRECATED @55 :Deprecated.LateralLQRState; indiStateDEPRECATED @52 :Deprecated.LateralINDIState; } @@ -867,6 +908,18 @@ struct ControlsState @0x97ff69c53601abf1 { saturated @3 :Bool; } + struct LateralCurvatureState @0xad9d8095c06f7c61 { + active @0 :Bool; + actualCurvature @1 :Float32; + desiredCurvature @2 :Float32; + error @3 :Float32; + p @4 :Float32; + i @5 :Float32; + f @6 :Float32; + output @7 :Float32; + saturated @8 :Bool; + } + deprecated :group { vEgo @0 :Float32; vEgoRaw @32 :Float32; @@ -906,7 +959,7 @@ struct ControlsState @0x97ff69c53601abf1 { alertStatus @38 :SelfdriveState.AlertStatus; alertSize @39 :SelfdriveState.AlertSize; alertType @44 :Text; - alertSound2 @56 :Car.CarControl.HUDControl.AudibleAlert; + alertSound2 @56 :SelfdriveState.AudibleAlert; engageable @41 :Bool; # can OP be engaged? state @31 :SelfdriveState.OpenpilotState; enabled @19 :Bool; @@ -989,7 +1042,6 @@ struct ModelDataV2 { roadEdgeStds @14 :List(Float32); # predicted lead cars - leads @11 :List(LeadDataV2); leadsV3 @18 :List(LeadDataV3); meta @12 :MetaData; @@ -999,8 +1051,9 @@ struct ModelDataV2 { action @26: Action; lateralPlannerSolutionDEPRECATED @25: Deprecated.LateralPlannerSolution; + leadsDEPRECATED @11 :List(LeadDataV2DEPRECATED); - struct LeadDataV2 { + struct LeadDataV2DEPRECATED { prob @0 :Float32; # probability that car is your lead at time t t @1 :Float32; @@ -1120,7 +1173,7 @@ struct EncodeIndex { } } -struct AndroidLogEntry { +struct OperatingSystemLogEntry { id @0 :UInt8; ts @1 :UInt64; priority @2 :UInt8; @@ -1258,7 +1311,7 @@ struct LateralPlan @0xe1e9318e2ae8b51e { struct LiveLocationKalman { # More info on reference frames: - # https://github.com/commaai/openpilot/tree/master/common/transformations + # https://github.com/commaai/openpilot/tree/master/openpilot/common/transformations positionECEF @0 : Measurement; positionGeodetic @1 : Measurement; @@ -1315,7 +1368,7 @@ struct LiveLocationKalman { struct LivePose { # More info on reference frames: - # https://github.com/commaai/openpilot/tree/master/common/transformations + # https://github.com/commaai/openpilot/tree/master/openpilot/common/transformations orientationNED @0 :XYZMeasurement; velocityDevice @1 :XYZMeasurement; accelerationDevice @2 :XYZMeasurement; @@ -2108,8 +2161,11 @@ struct DriverMonitoringStateDEPRECATED @0xb83cda094a1da284 { struct DriverMonitoringState { lockout @0 :Bool; - alertCountLockoutPercent @1 :Int8; - alertTimeLockoutPercent @2 :Int8; + lockoutCount @15 :Int8; + lockoutMinutesRemaining @11 :Int8; + alert3Count @12 :Int8; + noResponseCount @13 :Int8; + noResponseForceDecel @14 :Bool; alwaysOn @3 :Bool; alwaysOnLockout @4 :Bool; @@ -2173,6 +2229,11 @@ struct DriverMonitoringState { calibratedPercent @0 :Int8; offset @1 :Float32; } + + deprecated :group { + alertCountLockoutPercent @1 :Int8; + alertTimeLockoutPercent @2 :Int8; + } } struct Boot { @@ -2247,6 +2308,7 @@ struct LiveDelayData { lateralDelayEstimateStd @5 :Float32; points @4 :List(Float32); calPerc @6 :Int8; + version @7 :Int32; enum Status { unestimated @0; @@ -2504,7 +2566,7 @@ struct Event { rawAudioData @147 :AudioData; # systems stuff - androidLog @20 :AndroidLogEntry; + operatingSystemLog @20 :OperatingSystemLogEntry; managerState @78 :ManagerState; procLog @33 :ProcLog; clocks @35 :Clocks; diff --git a/cereal/messaging/__init__.py b/openpilot/cereal/messaging/__init__.py similarity index 77% rename from cereal/messaging/__init__.py rename to openpilot/cereal/messaging/__init__.py index ae32bfd4cb..dcc54baeb0 100644 --- a/cereal/messaging/__init__.py +++ b/openpilot/cereal/messaging/__init__.py @@ -7,12 +7,43 @@ import os import capnp import time -from typing import Optional, List, Union, Dict +from typing import Union -from cereal import log -from cereal.services import SERVICE_LIST +from openpilot.cereal import log +from openpilot.cereal.services import SERVICE_LIST from openpilot.common.utils import MovingAverage +__all__ = ( + "NO_TRAVERSAL_LIMIT", + "Context", + "FrequencyTracker", + "IpcError", + "MultiplePublishersError", + "Poller", + "PubMaster", + "PubSocket", + "SocketEventHandle", + "SubMaster", + "SubSocket", + "delete_fake_prefix", + "drain_sock", + "drain_sock_raw", + "fake_event_handle", + "get_fake_prefix", + "log_from_bytes", + "new_message", + "pub_sock", + "recv_one", + "recv_one_or_none", + "recv_one_retry", + "recv_sock", + "reset_context", + "set_fake_prefix", + "sub_sock", + "toggle_fake_events", + "wait_for_one_event", +) + NO_TRAVERSAL_LIMIT = 2**64-1 @@ -22,8 +53,8 @@ def pub_sock(endpoint: str) -> PubSocket: return msgq.pub_sock(endpoint, segment_size) -def sub_sock(endpoint: str, poller: Optional[Poller] = None, addr: str = "127.0.0.1", - conflate: bool = False, timeout: Optional[int] = None) -> SubSocket: +def sub_sock(endpoint: str, poller: Poller | None = None, addr: str = "127.0.0.1", + conflate: bool = False, timeout: int | None = None) -> SubSocket: service = SERVICE_LIST.get(endpoint) segment_size = service.queue_size if service else 0 return msgq.sub_sock(endpoint, poller=poller, addr=addr, conflate=conflate, @@ -39,7 +70,7 @@ def log_from_bytes(dat: bytes, struct: capnp.lib.capnp._StructModule = log.Event return msg -def new_message(service: Optional[str], size: Optional[int] = None, **kwargs) -> capnp.lib.capnp._DynamicStructBuilder: +def new_message(service: str | None, size: int | None = None, **kwargs) -> capnp.lib.capnp._DynamicStructBuilder: args = { 'valid': False, 'logMonoTime': int(time.monotonic() * 1e9), @@ -54,14 +85,14 @@ def new_message(service: Optional[str], size: Optional[int] = None, **kwargs) -> return dat -def drain_sock(sock: SubSocket, wait_for_one: bool = False) -> List[capnp.lib.capnp._DynamicStructReader]: +def drain_sock(sock: SubSocket, wait_for_one: bool = False) -> list[capnp.lib.capnp._DynamicStructReader]: """Receive all message currently available on the queue""" msgs = drain_sock_raw(sock, wait_for_one=wait_for_one) return [log_from_bytes(m) for m in msgs] # TODO: print when we drop packets? -def recv_sock(sock: SubSocket, wait: bool = False) -> Optional[capnp.lib.capnp._DynamicStructReader]: +def recv_sock(sock: SubSocket, wait: bool = False) -> capnp.lib.capnp._DynamicStructReader | None: """Same as drain sock, but only returns latest message. Consider using conflate instead.""" dat = None @@ -82,14 +113,14 @@ def recv_sock(sock: SubSocket, wait: bool = False) -> Optional[capnp.lib.capnp._ return dat -def recv_one(sock: SubSocket) -> Optional[capnp.lib.capnp._DynamicStructReader]: +def recv_one(sock: SubSocket) -> capnp.lib.capnp._DynamicStructReader | None: dat = sock.receive() if dat is not None: dat = log_from_bytes(dat) return dat -def recv_one_or_none(sock: SubSocket) -> Optional[capnp.lib.capnp._DynamicStructReader]: +def recv_one_or_none(sock: SubSocket) -> capnp.lib.capnp._DynamicStructReader | None: dat = sock.receive(non_blocking=True) if dat is not None: dat = log_from_bytes(dat) @@ -148,27 +179,27 @@ class FrequencyTracker: class SubMaster: - def __init__(self, services: List[str], poll: Optional[str] = None, - ignore_alive: Optional[List[str]] = None, ignore_avg_freq: Optional[List[str]] = None, - ignore_valid: Optional[List[str]] = None, addr: str = "127.0.0.1", frequency: Optional[float] = None): + def __init__(self, services: list[str], poll: str | None = None, + ignore_alive: list[str] | None = None, ignore_avg_freq: list[str] | None = None, + ignore_valid: list[str] | None = None, addr: str = "127.0.0.1", frequency: float | None = None): self.frame = -1 self.services = services - self.seen = {s: False for s in services} - self.updated = {s: False for s in services} - self.recv_time = {s: 0. for s in services} - self.recv_frame = {s: 0 for s in services} + self.seen = dict.fromkeys(services, False) + self.updated = dict.fromkeys(services, False) + self.recv_time = dict.fromkeys(services, 0.0) + self.recv_frame = dict.fromkeys(services, 0) self.sock = {} self.data = {} - self.logMonoTime = {s: 0 for s in services} + self.logMonoTime = dict.fromkeys(services, 0) # zero-frequency / on-demand services are always alive and presumed valid; all others must pass checks on_demand = {s: SERVICE_LIST[s].frequency <= 1e-5 for s in services} - self.static_freq_services = set(s for s in services if not on_demand[s]) + self.static_freq_services = {s for s in services if not on_demand[s]} self.alive = {s: on_demand[s] for s in services} self.freq_ok = {s: on_demand[s] for s in services} self.valid = {s: on_demand[s] for s in services} - self.freq_tracker: Dict[str, FrequencyTracker] = {} + self.freq_tracker: dict[str, FrequencyTracker] = {} self.poller = Poller() polled_services = set([poll, ] if poll is not None else services) self.non_polled_services = set(services) - polled_services @@ -211,7 +242,7 @@ class SubMaster: msgs.append(recv_one_or_none(self.sock[s])) self.update_msgs(time.monotonic(), msgs) - def update_msgs(self, cur_time: float, msgs: List[capnp.lib.capnp._DynamicStructReader]) -> None: + def update_msgs(self, cur_time: float, msgs: list[capnp.lib.capnp._DynamicStructReader]) -> None: self.frame += 1 self.updated = dict.fromkeys(self.services, False) for msg in msgs: @@ -234,21 +265,21 @@ class SubMaster: self.alive[s] = (cur_time - self.recv_time[s]) < (10. / SERVICE_LIST[s].frequency) or (self.seen[s] and self.simulation) self.freq_ok[s] = self.freq_tracker[s].valid or self.simulation - def all_alive(self, service_list: Optional[List[str]] = None) -> bool: + def all_alive(self, service_list: list[str] | None = None) -> bool: return all(self.alive[s] for s in (service_list or self.services) if s not in self.ignore_alive) - def all_freq_ok(self, service_list: Optional[List[str]] = None) -> bool: + def all_freq_ok(self, service_list: list[str] | None = None) -> bool: return all(self.freq_ok[s] for s in (service_list or self.services) if self._check_avg_freq(s)) - def all_valid(self, service_list: Optional[List[str]] = None) -> bool: + def all_valid(self, service_list: list[str] | None = None) -> bool: return all(self.valid[s] for s in (service_list or self.services) if s not in self.ignore_valid) - def all_checks(self, service_list: Optional[List[str]] = None) -> bool: + def all_checks(self, service_list: list[str] | None = None) -> bool: return self.all_alive(service_list) and self.all_freq_ok(service_list) and self.all_valid(service_list) class PubMaster: - def __init__(self, services: List[str]): + def __init__(self, services: list[str]): self.sock = {} for s in services: self.sock[s] = pub_sock(s) diff --git a/cereal/messaging/bridge.cc b/openpilot/cereal/messaging/bridge.cc similarity index 95% rename from cereal/messaging/bridge.cc rename to openpilot/cereal/messaging/bridge.cc index 77823c413a..ee3d5e93ca 100644 --- a/cereal/messaging/bridge.cc +++ b/openpilot/cereal/messaging/bridge.cc @@ -1,7 +1,7 @@ #include -#include "cereal/messaging/msgq_to_zmq.h" -#include "cereal/services.h" +#include "openpilot/cereal/messaging/msgq_to_zmq.h" +#include "openpilot/cereal/services.h" #include "common/util.h" ExitHandler do_exit; diff --git a/cereal/messaging/bridge_zmq.cc b/openpilot/cereal/messaging/bridge_zmq.cc similarity index 98% rename from cereal/messaging/bridge_zmq.cc rename to openpilot/cereal/messaging/bridge_zmq.cc index 5c56673b47..72d4345d03 100644 --- a/cereal/messaging/bridge_zmq.cc +++ b/openpilot/cereal/messaging/bridge_zmq.cc @@ -1,4 +1,4 @@ -#include "cereal/messaging/bridge_zmq.h" +#include "openpilot/cereal/messaging/bridge_zmq.h" #include #include diff --git a/cereal/messaging/bridge_zmq.h b/openpilot/cereal/messaging/bridge_zmq.h similarity index 100% rename from cereal/messaging/bridge_zmq.h rename to openpilot/cereal/messaging/bridge_zmq.h diff --git a/cereal/messaging/messaging.h b/openpilot/cereal/messaging/messaging.h similarity index 98% rename from cereal/messaging/messaging.h rename to openpilot/cereal/messaging/messaging.h index fb9c261f2b..933e0b9f15 100644 --- a/cereal/messaging/messaging.h +++ b/openpilot/cereal/messaging/messaging.h @@ -8,7 +8,7 @@ #include -#include "cereal/gen/cpp/log.capnp.h" +#include "openpilot/cereal/gen/cpp/log.capnp.h" #include "common/timing.h" #include "msgq/ipc.h" diff --git a/cereal/messaging/msgq_to_zmq.cc b/openpilot/cereal/messaging/msgq_to_zmq.cc similarity index 98% rename from cereal/messaging/msgq_to_zmq.cc rename to openpilot/cereal/messaging/msgq_to_zmq.cc index 5e7ea22273..adea00f923 100644 --- a/cereal/messaging/msgq_to_zmq.cc +++ b/openpilot/cereal/messaging/msgq_to_zmq.cc @@ -1,8 +1,8 @@ -#include "cereal/messaging/msgq_to_zmq.h" +#include "openpilot/cereal/messaging/msgq_to_zmq.h" #include -#include "cereal/services.h" +#include "openpilot/cereal/services.h" #include "common/util.h" extern ExitHandler do_exit; diff --git a/cereal/messaging/msgq_to_zmq.h b/openpilot/cereal/messaging/msgq_to_zmq.h similarity index 94% rename from cereal/messaging/msgq_to_zmq.h rename to openpilot/cereal/messaging/msgq_to_zmq.h index 64f3a2173e..a34f0ebbf0 100644 --- a/cereal/messaging/msgq_to_zmq.h +++ b/openpilot/cereal/messaging/msgq_to_zmq.h @@ -8,7 +8,7 @@ #include #include "msgq/impl_msgq.h" -#include "cereal/messaging/bridge_zmq.h" +#include "openpilot/cereal/messaging/bridge_zmq.h" class MsgqToZmq { public: diff --git a/cereal/messaging/socketmaster.cc b/openpilot/cereal/messaging/socketmaster.cc similarity index 98% rename from cereal/messaging/socketmaster.cc rename to openpilot/cereal/messaging/socketmaster.cc index dfeeb807ee..dd285a0c8e 100644 --- a/cereal/messaging/socketmaster.cc +++ b/openpilot/cereal/messaging/socketmaster.cc @@ -3,8 +3,8 @@ #include #include -#include "cereal/services.h" -#include "cereal/messaging/messaging.h" +#include "openpilot/cereal/services.h" +#include "openpilot/cereal/messaging/messaging.h" const bool SIMULATION = (getenv("SIMULATION") != nullptr) && (std::string(getenv("SIMULATION")) == "1"); diff --git a/cereal/messaging/tests/__init__.py b/openpilot/cereal/messaging/tests/__init__.py similarity index 100% rename from cereal/messaging/tests/__init__.py rename to openpilot/cereal/messaging/tests/__init__.py diff --git a/cereal/messaging/tests/test_messaging.py b/openpilot/cereal/messaging/tests/test_messaging.py similarity index 82% rename from cereal/messaging/tests/test_messaging.py rename to openpilot/cereal/messaging/tests/test_messaging.py index 97388446f4..92d77f1b35 100644 --- a/cereal/messaging/tests/test_messaging.py +++ b/openpilot/cereal/messaging/tests/test_messaging.py @@ -1,4 +1,3 @@ -import os import capnp import multiprocessing import numbers @@ -6,11 +5,11 @@ import random import threading import time from openpilot.common.parameterized import parameterized -import pytest -from cereal import log, car -import cereal.messaging as messaging -from cereal.services import SERVICE_LIST +from openpilot.cereal import log +from opendbc.car.structs import car +import openpilot.cereal.messaging as messaging +from openpilot.cereal.services import SERVICE_LIST events = [evt for evt in log.Event.schema.union_fields if evt in SERVICE_LIST.keys()] @@ -23,10 +22,6 @@ def random_socks(num_socks=10): def random_bytes(length=1000): return bytes([random.randrange(0xFF) for _ in range(length)]) -def zmq_sleep(t=1): - if "ZMQ" in os.environ: - time.sleep(t) - # TODO: this should take any capnp struct and returrn a msg with random populated data def random_carstate(): @@ -52,16 +47,6 @@ def delayed_send(delay, sock, dat): class TestMessaging: - def setUp(self): - # TODO: ZMQ tests are too slow; all sleeps will need to be - # replaced with logic to block on the necessary condition - if "ZMQ" in os.environ: - pytest.skip() - - # ZMQ pub socket takes too long to die - # sleep to prevent multiple publishers error between tests - zmq_sleep() - @parameterized.expand(events) def test_new_message(self, evt): try: @@ -88,7 +73,6 @@ class TestMessaging: sock = "carState" pub_sock = messaging.pub_sock(sock) sub_sock = messaging.sub_sock(sock, timeout=1000) - zmq_sleep() # no wait and no msgs in queue msgs = func(sub_sock) @@ -109,7 +93,6 @@ class TestMessaging: sock = "carState" pub_sock = messaging.pub_sock(sock) sub_sock = messaging.sub_sock(sock, timeout=100) - zmq_sleep() # no wait and no msg in queue, socket should timeout recvd = messaging.recv_sock(sub_sock) @@ -128,7 +111,6 @@ class TestMessaging: sock = "carState" pub_sock = messaging.pub_sock(sock) sub_sock = messaging.sub_sock(sock, timeout=1000) - zmq_sleep() # no msg in queue, socket should timeout recvd = messaging.recv_one(sub_sock) @@ -141,12 +123,10 @@ class TestMessaging: assert isinstance(recvd, capnp._DynamicStructReader) assert_carstate(msg.carState, recvd.carState) - @pytest.mark.xfail(condition="ZMQ" in os.environ, reason='ZMQ detected') def test_recv_one_or_none(self): sock = "carState" pub_sock = messaging.pub_sock(sock) sub_sock = messaging.sub_sock(sock) - zmq_sleep() # no msg in queue, socket shouldn't block recvd = messaging.recv_one_or_none(sub_sock) @@ -164,16 +144,13 @@ class TestMessaging: sock_timeout = 0.1 pub_sock = messaging.pub_sock(sock) sub_sock = messaging.sub_sock(sock, timeout=round(sock_timeout*1000)) - zmq_sleep() - # this test doesn't work with ZMQ since multiprocessing interrupts it - if "ZMQ" not in os.environ: - # wait 5 socket timeouts and make sure it's still retrying - p = multiprocessing.Process(target=messaging.recv_one_retry, args=(sub_sock,)) - p.start() - time.sleep(sock_timeout*5) - assert p.is_alive() - p.terminate() + # wait 5 socket timeouts and make sure it's still retrying + p = multiprocessing.Process(target=messaging.recv_one_retry, args=(sub_sock,)) + p.start() + time.sleep(sock_timeout*5) + assert p.is_alive() + p.terminate() # wait 5 socket timeouts before sending msg = random_carstate() diff --git a/cereal/messaging/tests/test_pub_sub_master.py b/openpilot/cereal/messaging/tests/test_pub_sub_master.py similarity index 84% rename from cereal/messaging/tests/test_pub_sub_master.py rename to openpilot/cereal/messaging/tests/test_pub_sub_master.py index 5e26b49701..7353764aea 100644 --- a/cereal/messaging/tests/test_pub_sub_master.py +++ b/openpilot/cereal/messaging/tests/test_pub_sub_master.py @@ -1,21 +1,16 @@ import random import time -from typing import Sized, cast +from typing import cast +from collections.abc import Sized -import cereal.messaging as messaging -from cereal.messaging.tests.test_messaging import events, random_sock, random_socks, \ - random_bytes, random_carstate, assert_carstate, \ - zmq_sleep -from cereal.services import SERVICE_LIST +import openpilot.cereal.messaging as messaging +from openpilot.cereal.messaging.tests.test_messaging import events, random_sock, random_socks, \ + random_bytes, random_carstate, assert_carstate +from openpilot.cereal.services import SERVICE_LIST class TestSubMaster: - def setup_method(self): - # ZMQ pub socket takes too long to die - # sleep to prevent multiple publishers error between tests - zmq_sleep(3) - def test_init(self): sm = messaging.SubMaster(events) for p in [sm.updated, sm.recv_time, sm.recv_frame, sm.alive, @@ -42,7 +37,6 @@ class TestSubMaster: sock = "carState" pub_sock = messaging.pub_sock(sock) sm = messaging.SubMaster([sock,]) - zmq_sleep() msg = random_carstate() pub_sock.send(msg.to_bytes()) @@ -54,7 +48,6 @@ class TestSubMaster: sock = "carState" pub_sock = messaging.pub_sock(sock) sm = messaging.SubMaster([sock,]) - zmq_sleep() for i in range(10): msg = messaging.new_message(sock) @@ -91,21 +84,13 @@ class TestSubMaster: for service, (max_freq, min_freq) in checks.items(): if max_freq is not None: + assert min_freq is not None assert sm._check_avg_freq(service) assert sm.freq_tracker[service].max_freq == max_freq*1.2 assert sm.freq_tracker[service].min_freq == min_freq*0.8 else: assert not sm._check_avg_freq(service) - def test_alive(self): - pass - - def test_ignore_alive(self): - pass - - def test_valid(self): - pass - # SubMaster should always conflate def test_conflate(self): sock = "carState" @@ -124,11 +109,6 @@ class TestSubMaster: class TestPubMaster: - def setup_method(self): - # ZMQ pub socket takes too long to die - # sleep to prevent multiple publishers error between tests - zmq_sleep(3) - def test_init(self): messaging.PubMaster(events) @@ -136,7 +116,6 @@ class TestPubMaster: socks = random_socks() pm = messaging.PubMaster(socks) sub_socks = {s: messaging.sub_sock(s, conflate=True, timeout=1000) for s in socks} - zmq_sleep() # PubMaster accepts either a capnp msg builder or bytes for capnp in [True, False]: diff --git a/cereal/messaging/tests/test_services.py b/openpilot/cereal/messaging/tests/test_services.py similarity index 64% rename from cereal/messaging/tests/test_services.py rename to openpilot/cereal/messaging/tests/test_services.py index 3320723fec..9ae7c3d840 100644 --- a/cereal/messaging/tests/test_services.py +++ b/openpilot/cereal/messaging/tests/test_services.py @@ -1,10 +1,10 @@ -import os +import subprocess import tempfile -from typing import Dict + from openpilot.common.parameterized import parameterized -import cereal.services as services -from cereal.services import SERVICE_LIST +import openpilot.cereal.services as services +from openpilot.cereal.services import SERVICE_LIST class TestServices: @@ -17,5 +17,5 @@ class TestServices: def test_generated_header(self): with tempfile.NamedTemporaryFile(suffix=".h") as f: - ret = os.system(f"python3 {services.__file__} > {f.name} && clang++ {f.name} -std=c++11") + ret = subprocess.run(f"python3 {services.__file__} > {f.name} && clang++ {f.name} -std=c++11", shell=True).returncode assert ret == 0, "generated services header is not valid C" diff --git a/openpilot/cereal/messaging/tests/validate_sp_cereal_upstream.py b/openpilot/cereal/messaging/tests/validate_sp_cereal_upstream.py new file mode 100755 index 0000000000..543b8b1b3e --- /dev/null +++ b/openpilot/cereal/messaging/tests/validate_sp_cereal_upstream.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Validate sunnypilot routes are parseable by stock commaai/openpilot. + +Cap'n Proto is wire-compatible across renames, type relocations, and +additive fields. The only breaking change is a union variant that +upstream doesn't recognize — an unknown discriminant makes the entire +union unreadable. + +This script checks: for every struct with a union that exists in both +schemas, does sunnypilot introduce union variants upstream doesn't have? +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +from typing import Any + +NO_DISCRIMINANT = 0xFFFF + + +def hex_id(value: int) -> str: + return f"0x{value:016x}" + + +def encode_struct(schema: Any) -> dict: + node = schema.node + fields = [] + for name, field in schema.fields.items(): + proto = field.proto + ordinal = proto.ordinal.explicit if proto.ordinal.which() == "explicit" else None + discriminant = proto.discriminantValue if proto.discriminantValue != NO_DISCRIMINANT else None + fields.append({"name": name, "ordinal": ordinal, "discriminant": discriminant}) + return { + "typeId": hex_id(node.id), + "displayName": node.displayName, + "hasUnion": node.struct.discriminantCount > 0, + "fields": fields, + } + + +def _child_struct_schema(field: Any) -> Any: + proto = field.proto + if proto.which() == "group": + return field.schema + type_node = proto.slot.type + which = type_node.which() + if which == "struct": + return field.schema + if which == "list": + container = field.schema + element_type = type_node.list.elementType + while element_type.which() == "list": + container = container.elementType + element_type = element_type.list.elementType + if element_type.which() == "struct": + return container.elementType + return None + + +def collect_schema(root: Any) -> dict[str, dict]: + structs: dict[str, dict] = {} + stack = [root] + while stack: + schema = stack.pop() + type_id = hex_id(schema.node.id) + if type_id in structs: + continue + structs[type_id] = encode_struct(schema) + for _name, field in schema.fields.items(): + try: + child = _child_struct_schema(field) + except Exception: + child = None + if child is not None: + stack.append(child) + return structs + + +def load_log(cereal_dir: str, extra_imports: list[str] | None = None) -> Any: + import capnp + cereal_dir = os.path.abspath(cereal_dir) + capnp.remove_import_hook() + imports = [cereal_dir] + [os.path.abspath(p) for p in (extra_imports or [])] + return capnp.load(os.path.join(cereal_dir, "log.capnp"), imports=imports) + + +def dump_schema(cereal_dir: str, path: str, extra_imports: list[str] | None = None) -> None: + log = load_log(cereal_dir, extra_imports) + payload = { + "root": hex_id(log.Event.schema.node.id), + "structs": collect_schema(log.Event.schema), + } + with open(path, "w", encoding="utf-8") as handle: + json.dump(payload, handle, indent=2, sort_keys=True) + print(f"wrote schema dump with {len(payload['structs'])} structs to {path}") + + +def compare(sunnypilot_dump: dict, upstream_dump: dict) -> list[str]: + violations: list[str] = [] + sunnypilot_structs = sunnypilot_dump["structs"] + upstream_structs = upstream_dump["structs"] + + for type_id, sp_struct in sunnypilot_structs.items(): + if not sp_struct["hasUnion"]: + continue + up_struct = upstream_structs.get(type_id) + if up_struct is None: + continue + + up_ordinals = {f["ordinal"] for f in up_struct["fields"] if f.get("discriminant") is not None} + display = sp_struct["displayName"] + + for field in sp_struct["fields"]: + if field.get("discriminant") is None: + continue + if field["ordinal"] not in up_ordinals: + violations.append( + f"{display} @{field['ordinal']} '{field['name']}': " + f"union variant not present upstream (discriminant={field['discriminant']})" + ) + + return violations + + +def run_read(cereal_dir: str, peer_path: str, extra_imports: list[str] | None = None) -> int: + log = load_log(cereal_dir, extra_imports) + with open(peer_path, "r", encoding="utf-8") as f: + peer_dump = json.load(f) + local_dump = { + "root": hex_id(log.Event.schema.node.id), + "structs": collect_schema(log.Event.schema), + } + violations = compare(sunnypilot_dump=peer_dump, upstream_dump=local_dump) + + if not violations: + print("cereal compat OK: upstream can parse sunnypilot routes.") + return 0 + + print(f"cereal compat FAIL ({len(violations)} leaked union variant(s)):") + for v in violations: + print(f" {v}") + return 1 + + +def main() -> int: + parser = argparse.ArgumentParser(description="sunnypilot cereal upstream compat check") + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument("-g", "--generate", action="store_true", help="dump local schema to JSON") + mode.add_argument("-r", "--read", action="store_true", help="validate against peer schema") + parser.add_argument("-f", "--file", default="schema.json", help="JSON file path") + parser.add_argument("--cereal-dir", required=True, help="path to cereal directory") + parser.add_argument("-I", "--import-path", action="append", default=[], help="extra capnp import paths") + args = parser.parse_args() + + if args.generate: + dump_schema(args.cereal_dir, args.file, args.import_path) + return 0 + return run_read(args.cereal_dir, args.file, args.import_path) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/cereal/services.py b/openpilot/cereal/services.py similarity index 95% rename from cereal/services.py rename to openpilot/cereal/services.py index f933fc0125..88931b480d 100755 --- a/cereal/services.py +++ b/openpilot/cereal/services.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 from enum import IntEnum -from typing import Optional # TODO: this should be automatically determined using the capnp schema @@ -11,7 +10,7 @@ class QueueSize(IntEnum): class Service: - def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] = None, + def __init__(self, should_log: bool, frequency: float, decimation: int | None = None, queue_size: QueueSize = QueueSize.SMALL): self.should_log = should_log self.frequency = frequency @@ -41,7 +40,7 @@ _services: dict[str, tuple] = { "liveCalibration": (True, 4., 4), "liveTorqueParameters": (True, 4., 1), "liveDelay": (True, 4., 1), - "androidLog": (True, 0.), + "operatingSystemLog": (True, 0.), "carState": (True, 100., 10), "carControl": (True, 100., 10), "carOutput": (True, 100., 10), @@ -125,8 +124,7 @@ def build_header(): for k, v in SERVICE_LIST.items(): should_log = "true" if v.should_log else "false" decimation = -1 if v.decimation is None else v.decimation - h += ' { "%s", {"%s", %s, %f, %d, %d}},\n' % \ - (k, k, should_log, v.frequency, decimation, v.queue_size) + h += f' {{ "{k}", {{"{k}", {should_log}, {v.frequency:f}, {decimation:d}, {v.queue_size:d}}}}},\n' h += "};\n" h += "#endif\n" diff --git a/openpilot/common b/openpilot/common deleted file mode 120000 index 60d3b0a6a8..0000000000 --- a/openpilot/common +++ /dev/null @@ -1 +0,0 @@ -../common \ No newline at end of file diff --git a/common/SConscript b/openpilot/common/SConscript similarity index 81% rename from common/SConscript rename to openpilot/common/SConscript index c9bd1c72d1..13935b80d5 100644 --- a/common/SConscript +++ b/openpilot/common/SConscript @@ -5,6 +5,7 @@ common_libs = [ 'swaglog.cc', 'util.cc', 'ratekeeper.cc', + 'yuv.cc', ] _common = env.Library('common', common_libs, LIBS="json11") @@ -12,7 +13,7 @@ Export('_common') if GetOption('extras'): env.Program('tests/test_common', - ['tests/test_runner.cc', 'tests/test_params.cc', 'tests/test_util.cc', 'tests/test_swaglog.cc'], + ['tests/test_runner.cc', 'tests/test_util.cc', 'tests/test_swaglog.cc'], LIBS=[_common, 'json11', 'zmq', 'pthread']) # Cython bindings diff --git a/common/__init__.py b/openpilot/common/__init__.py similarity index 100% rename from common/__init__.py rename to openpilot/common/__init__.py diff --git a/common/api/__init__.py b/openpilot/common/api/__init__.py similarity index 100% rename from common/api/__init__.py rename to openpilot/common/api/__init__.py diff --git a/common/api/base.py b/openpilot/common/api/base.py similarity index 95% rename from common/api/base.py rename to openpilot/common/api/base.py index 5cb59347ad..32dd5a1ed7 100644 --- a/common/api/base.py +++ b/openpilot/common/api/base.py @@ -3,8 +3,8 @@ import os import requests import unicodedata from datetime import datetime, timedelta, UTC -from openpilot.system.hardware.hw import Paths -from openpilot.system.version import get_version +from openpilot.common.hardware.hw import Paths +from openpilot.common.version import get_version # name: jwt signature algorithm KEYS = {"id_rsa": "RS256", @@ -38,6 +38,7 @@ class BaseApi: } if payload_extra is not None: payload.update(payload_extra) + assert self.private_key is not None token = jwt.encode(payload, self.private_key, algorithm=self.jwt_algorithm) if isinstance(token, bytes): token = token.decode('utf8') diff --git a/common/api/comma_connect.py b/openpilot/common/api/comma_connect.py similarity index 100% rename from common/api/comma_connect.py rename to openpilot/common/api/comma_connect.py diff --git a/common/basedir.py b/openpilot/common/basedir.py similarity index 71% rename from common/basedir.py rename to openpilot/common/basedir.py index 6b4811e53c..49b092d930 100644 --- a/common/basedir.py +++ b/openpilot/common/basedir.py @@ -1,4 +1,4 @@ import os -BASEDIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../")) +BASEDIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../..")) diff --git a/common/constants.py b/openpilot/common/constants.py similarity index 100% rename from common/constants.py rename to openpilot/common/constants.py diff --git a/openpilot/common/esim/__init__.py b/openpilot/common/esim/__init__.py new file mode 100644 index 0000000000..5f83412c07 --- /dev/null +++ b/openpilot/common/esim/__init__.py @@ -0,0 +1,3 @@ +from openpilot.common.esim.base import LPABase, LPAError, LPAProfileNotFoundError, Profile + +__all__ = ["LPABase", "LPAError", "LPAProfileNotFoundError", "Profile"] diff --git a/openpilot/common/esim/base.py b/openpilot/common/esim/base.py new file mode 100644 index 0000000000..b783a630b2 --- /dev/null +++ b/openpilot/common/esim/base.py @@ -0,0 +1,55 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass + + +class LPAError(RuntimeError): + pass + + +class LPAProfileNotFoundError(LPAError): + pass + + +@dataclass +class Profile: + iccid: str + nickname: str + enabled: bool + provider: str + + @property + def is_comma(self) -> bool: + return self.provider == 'Webbing' and self.iccid.startswith('8985235') + + +class LPABase(ABC): + @abstractmethod + def list_profiles(self) -> list[Profile]: + pass + + @abstractmethod + def get_active_profile(self) -> Profile | None: + pass + + @abstractmethod + def delete_profile(self, iccid: str) -> None: + pass + + @abstractmethod + def download_profile(self, qr: str, nickname: str | None = None) -> None: + pass + + @abstractmethod + def nickname_profile(self, iccid: str, nickname: str) -> None: + pass + + @abstractmethod + def switch_profile(self, iccid: str) -> None: + pass + + def process_notifications(self) -> None: + pass + + @abstractmethod + def is_euicc(self) -> bool: + pass diff --git a/system/hardware/esim.py b/openpilot/common/esim/esim.py similarity index 95% rename from system/hardware/esim.py rename to openpilot/common/esim/esim.py index 18d84de983..8456d4dd47 100755 --- a/system/hardware/esim.py +++ b/openpilot/common/esim/esim.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 import argparse -from openpilot.system.hardware import HARDWARE -from openpilot.system.hardware.base import LPABase, Profile +from openpilot.common.hardware import HARDWARE +from openpilot.common.esim.base import LPABase, Profile def sorted_profiles(lpa: LPABase) -> list[Profile]: diff --git a/system/hardware/tici/gsma_ci_bundle.pem b/openpilot/common/esim/gsma_ci_bundle.pem similarity index 100% rename from system/hardware/tici/gsma_ci_bundle.pem rename to openpilot/common/esim/gsma_ci_bundle.pem diff --git a/system/hardware/tici/lpa.py b/openpilot/common/esim/lpa.py similarity index 98% rename from system/hardware/tici/lpa.py rename to openpilot/common/esim/lpa.py index 618d11e5d1..80138b54ae 100644 --- a/system/hardware/tici/lpa.py +++ b/openpilot/common/esim/lpa.py @@ -6,7 +6,6 @@ import fcntl import hashlib import os import requests -import serial import subprocess import sys import termios @@ -19,7 +18,8 @@ from typing import Any from pathlib import Path from openpilot.common.time_helpers import system_time_valid -from openpilot.system.hardware.base import LPABase, LPAError, LPAProfileNotFoundError, Profile +from openpilot.common.esim.base import LPABase, LPAError, LPAProfileNotFoundError, Profile +from openpilot.common.serial import Serial, SerialException GSMA_CI_BUNDLE = str(Path(__file__).parent / "gsma_ci_bundle.pem") @@ -133,7 +133,7 @@ class AtClient: self._device = device self._baud = baud self._timeout = timeout - self._serial: serial.Serial | None = None + self._serial: Serial | None = None def send_raw(self, data: bytes) -> None: self._ensure_serial() @@ -185,14 +185,14 @@ class AtClient: pass self._serial = None if self._serial is None: - self._serial = serial.Serial(self._device, baudrate=self._baud, timeout=self._timeout) + self._serial = Serial(self._device, baudrate=self._baud, timeout=self._timeout) def query(self, cmd: str) -> list[str]: self._ensure_serial() try: self._send(cmd) return self._expect() - except serial.SerialException: + except SerialException: self._ensure_serial(reconnect=True) self._send(cmd) return self._expect() @@ -208,7 +208,7 @@ class AtClient: if self._serial: try: self._serial.reset_input_buffer() - except (OSError, serial.SerialException, termios.error): + except (OSError, SerialException, termios.error): self._ensure_serial(reconnect=True) for line in self.query(f'AT+CCHO="{ISDR_AID}"'): if line.startswith("+CCHO:") and (ch := line.split(":", 1)[1].strip()): @@ -230,7 +230,7 @@ class AtClient: try: self._open_isdr_once() return - except (RuntimeError, TimeoutError, termios.error, serial.SerialException): + except (RuntimeError, TimeoutError, termios.error, SerialException): time.sleep(OPEN_ISDR_RETRY_DELAY_S) if attempt == OPEN_ISDR_RESET_ATTEMPT: self._reset_modem() diff --git a/common/file_chunker.py b/openpilot/common/file_chunker.py similarity index 59% rename from common/file_chunker.py rename to openpilot/common/file_chunker.py index 57dfc35531..2d080c3fff 100755 --- a/common/file_chunker.py +++ b/openpilot/common/file_chunker.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import io import sys import math import os @@ -21,13 +22,12 @@ def get_chunk_targets(path, file_size): def chunk_file(path, targets): manifest_path, *chunk_paths = targets - with open(path, 'rb') as f: - data = f.read() - actual_num_chunks = max(1, math.ceil(len(data) / CHUNK_SIZE)) + actual_num_chunks = max(1, math.ceil(os.path.getsize(path) / CHUNK_SIZE)) assert len(chunk_paths) >= actual_num_chunks, f"Allowed {len(chunk_paths)} chunks but needs at least {actual_num_chunks}, for path {path}" - for i, chunk_path in enumerate(chunk_paths): - with open(chunk_path, 'wb') as f: - f.write(data[i * CHUNK_SIZE:(i + 1) * CHUNK_SIZE]) + with open(path, 'rb') as f: + for chunk_path in chunk_paths: + with open(chunk_path, 'wb') as out: + out.write(f.read(CHUNK_SIZE)) Path(manifest_path).write_text(str(len(chunk_paths))) os.remove(path) @@ -39,14 +39,41 @@ def get_existing_chunks(path): return _chunk_paths(path, num_chunks) raise FileNotFoundError(path) -def read_file_chunked(path): +class ChunkStream(io.RawIOBase): + def __init__(self, paths): + self._paths = iter(paths) + self._f = None + + def readable(self): + return True + + def readinto(self, b): + n = 0 + view = memoryview(b) + while n < len(b): + if self._f is None: + p = next(self._paths, None) + if p is None: + break + self._f = open(p, 'rb') + count = self._f.readinto(view[n:]) + if not count: + self._f.close() + self._f = None + continue + n += count + return n + +def open_file_chunked(path): manifest_path = get_manifest_path(path) if os.path.isfile(manifest_path): num_chunks = int(Path(manifest_path).read_text().strip()) - return b''.join(Path(get_chunk_name(path, i, num_chunks)).read_bytes() for i in range(num_chunks)) - if os.path.isfile(path): - return Path(path).read_bytes() - raise FileNotFoundError(path) + paths = [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)] + elif os.path.isfile(path): + paths = [path] + else: + raise FileNotFoundError(path) + return io.BufferedReader(ChunkStream(paths)) if __name__ == "__main__": diff --git a/common/filter_simple.py b/openpilot/common/filter_simple.py similarity index 100% rename from common/filter_simple.py rename to openpilot/common/filter_simple.py diff --git a/common/git.py b/openpilot/common/git.py similarity index 100% rename from common/git.py rename to openpilot/common/git.py diff --git a/common/gpio.py b/openpilot/common/gpio.py similarity index 100% rename from common/gpio.py rename to openpilot/common/gpio.py diff --git a/common/gps.py b/openpilot/common/gps.py similarity index 100% rename from common/gps.py rename to openpilot/common/gps.py diff --git a/system/hardware/__init__.py b/openpilot/common/hardware/__init__.py similarity index 55% rename from system/hardware/__init__.py rename to openpilot/common/hardware/__init__.py index 99079b5ef3..3387d8a0ca 100644 --- a/system/hardware/__init__.py +++ b/openpilot/common/hardware/__init__.py @@ -1,9 +1,9 @@ import os from typing import cast -from openpilot.system.hardware.base import HardwareBase -from openpilot.system.hardware.tici.hardware import Tici -from openpilot.system.hardware.pc.hardware import Pc +from openpilot.common.hardware.base import HardwareBase +from openpilot.common.hardware.tici.hardware import Tici +from openpilot.common.hardware.pc.hardware import Pc TICI = os.path.isfile('/TICI') AGNOS = os.path.isfile('/AGNOS') diff --git a/system/hardware/base.h b/openpilot/common/hardware/base.h similarity index 72% rename from system/hardware/base.h rename to openpilot/common/hardware/base.h index 3eded659ac..f4546adfa8 100644 --- a/system/hardware/base.h +++ b/openpilot/common/hardware/base.h @@ -5,15 +5,13 @@ #include #include -#include "cereal/gen/cpp/log.capnp.h" +#include "openpilot/cereal/gen/cpp/log.capnp.h" // no-op base hw class class HardwareNone { public: static std::string get_name() { return ""; } static cereal::InitData::DeviceType get_device_type() { return cereal::InitData::DeviceType::UNKNOWN; } - static int get_voltage() { return 0; } - static int get_current() { return 0; } static std::string get_serial() { return "cccccc"; } @@ -24,6 +22,4 @@ public: static void set_ir_power(int percentage) {} static bool PC() { return false; } - static bool TICI() { return false; } - static bool AGNOS() { return false; } }; diff --git a/system/hardware/base.py b/openpilot/common/hardware/base.py similarity index 75% rename from system/hardware/base.py rename to openpilot/common/hardware/base.py index 5bcf886ae8..5d8f7770dc 100644 --- a/system/hardware/base.py +++ b/openpilot/common/hardware/base.py @@ -2,28 +2,12 @@ import os from abc import abstractmethod, ABC from dataclasses import dataclass, fields -from cereal import log +from openpilot.cereal import log +from openpilot.common.esim.base import LPABase NetworkType = log.DeviceState.NetworkType NetworkStrength = log.DeviceState.NetworkStrength -class LPAError(RuntimeError): - pass - -class LPAProfileNotFoundError(LPAError): - pass - -@dataclass -class Profile: - iccid: str - nickname: str - enabled: bool - provider: str - - @property - def is_comma(self) -> bool: - return self.provider == 'Webbing' and self.iccid.startswith('8985235') - @dataclass class ThermalZone: # a zone from /sys/class/thermal/thermal_zone* @@ -70,38 +54,6 @@ class ThermalConfig: ret[f.name + "TempC"] = v.read() return ret -class LPABase(ABC): - @abstractmethod - def list_profiles(self) -> list[Profile]: - pass - - @abstractmethod - def get_active_profile(self) -> Profile | None: - pass - - @abstractmethod - def delete_profile(self, iccid: str) -> None: - pass - - @abstractmethod - def download_profile(self, qr: str, nickname: str | None = None) -> None: - pass - - @abstractmethod - def nickname_profile(self, iccid: str, nickname: str) -> None: - pass - - @abstractmethod - def switch_profile(self, iccid: str) -> None: - pass - - def process_notifications(self) -> None: - pass - - @abstractmethod - def is_euicc(self) -> bool: - pass - class HardwareBase(ABC): @staticmethod def get_cmdline() -> dict[str, str]: @@ -133,7 +85,7 @@ class HardwareBase(ABC): def get_device_type(self): pass - def get_imei(self, slot) -> str: + def get_imei(self) -> str: return "" def get_serial(self): @@ -190,21 +142,12 @@ class HardwareBase(ABC): def get_gpu_usage_percent(self): return 0 - def get_modem_version(self): - return None - def get_modem_temperatures(self): return [] def initialize_hardware(self): pass - def get_networks(self): - return None - - def has_internal_panda(self) -> bool: - return False - def reset_internal_panda(self): pass @@ -214,11 +157,5 @@ class HardwareBase(ABC): def get_modem_data_usage(self): return -1, -1 - def get_voltage(self) -> float: - return 0. - - def get_current(self) -> float: - return 0. - def set_ir_power(self, percent: int): pass diff --git a/system/hardware/hw.h b/openpilot/common/hardware/hw.h similarity index 92% rename from system/hardware/hw.h rename to openpilot/common/hardware/hw.h index c9af7e8a33..7681479ccc 100644 --- a/system/hardware/hw.h +++ b/openpilot/common/hardware/hw.h @@ -2,14 +2,14 @@ #include -#include "system/hardware/base.h" +#include "common/hardware/base.h" #include "common/util.h" #if __TICI__ -#include "system/hardware/tici/hardware.h" +#include "common/hardware/tici/hardware.h" #define Hardware HardwareTici #else -#include "system/hardware/pc/hardware.h" +#include "common/hardware/pc/hardware.h" #define Hardware HardwarePC #endif diff --git a/system/hardware/hw.py b/openpilot/common/hardware/hw.py similarity index 98% rename from system/hardware/hw.py rename to openpilot/common/hardware/hw.py index 3527aac872..245856fa16 100644 --- a/system/hardware/hw.py +++ b/openpilot/common/hardware/hw.py @@ -2,7 +2,7 @@ import os import platform from pathlib import Path -from openpilot.system.hardware import PC +from openpilot.common.hardware import PC DEFAULT_DOWNLOAD_CACHE_ROOT = "/tmp/comma_download_cache" diff --git a/common/tests/__init__.py b/openpilot/common/hardware/pc/__init__.py similarity index 100% rename from common/tests/__init__.py rename to openpilot/common/hardware/pc/__init__.py diff --git a/system/hardware/pc/hardware.h b/openpilot/common/hardware/pc/hardware.h similarity index 63% rename from system/hardware/pc/hardware.h rename to openpilot/common/hardware/pc/hardware.h index 71f58b188b..2c43b43292 100644 --- a/system/hardware/pc/hardware.h +++ b/openpilot/common/hardware/pc/hardware.h @@ -2,13 +2,11 @@ #include -#include "system/hardware/base.h" +#include "common/hardware/base.h" class HardwarePC : public HardwareNone { public: static std::string get_name() { return "pc"; } static cereal::InitData::DeviceType get_device_type() { return cereal::InitData::DeviceType::PC; } static bool PC() { return true; } - static bool TICI() { return util::getenv("TICI", 0) == 1; } - static bool AGNOS() { return util::getenv("TICI", 0) == 1; } }; diff --git a/openpilot/common/hardware/pc/hardware.py b/openpilot/common/hardware/pc/hardware.py new file mode 100644 index 0000000000..b4aa56c904 --- /dev/null +++ b/openpilot/common/hardware/pc/hardware.py @@ -0,0 +1,10 @@ +from openpilot.cereal import log +from openpilot.common.hardware.base import HardwareBase + +class Pc(HardwareBase): + def get_device_type(self): + return "pc" + + def get_network_type(self): + # some stuff is gated on wifi, so just assume for now + return log.DeviceState.NetworkType.wifi diff --git a/openpilot/common/hardware/tici/__init__.py b/openpilot/common/hardware/tici/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/openpilot/common/hardware/tici/__init__.py @@ -0,0 +1 @@ + diff --git a/system/hardware/tici/agnos.json b/openpilot/common/hardware/tici/agnos.json similarity index 75% rename from system/hardware/tici/agnos.json rename to openpilot/common/hardware/tici/agnos.json index 07e2079ec9..f22a57db75 100644 --- a/system/hardware/tici/agnos.json +++ b/openpilot/common/hardware/tici/agnos.json @@ -56,28 +56,28 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8.img.xz", - "hash": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8", - "hash_raw": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8", + "url": "https://commadist.azureedge.net/agnosupdate/boot-19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39.img.xz", + "hash": "19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39", + "hash_raw": "19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39", "size": 17487872, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "edca8bee1531e66953d107eeceeed2dc7b3ca46417e49d55508f94e58bf95db8" + "ondevice_hash": "ddfe93cc6a8531af92ee331d9bbaeae2f1d933bdb38e579769dc9fe7998eb626" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img.xz", - "hash": "78acfe16a7b62a3a91fc7a81f40a693e4468cec1c69df7d0b1e550aacc646113", - "hash_raw": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f", + "url": "https://commadist.azureedge.net/agnosupdate/system-a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724.img.xz", + "hash": "4dc41c2c072f5f5d5cd484cd6173049cd96acfb9a67bc20049775585fe881539", + "hash_raw": "a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724", "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "743142c5a898f27b2a1029cca42c8a5d5d1fc0096414422b850fe84c8d0b8342", + "ondevice_hash": "cf1229630b7a2b8497705bca4ba947dbf0c217418ff4febff571aa4f4a878134", "alt": { - "hash": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f", - "url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img", + "hash": "a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724", + "url": "https://commadist.azureedge.net/agnosupdate/system-a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724.img", "size": 4718592000 } } diff --git a/system/hardware/tici/agnos.py b/openpilot/common/hardware/tici/agnos.py similarity index 80% rename from system/hardware/tici/agnos.py rename to openpilot/common/hardware/tici/agnos.py index f5261953d5..b3b5e05176 100755 --- a/system/hardware/tici/agnos.py +++ b/openpilot/common/hardware/tici/agnos.py @@ -10,19 +10,16 @@ from collections.abc import Generator import requests -import openpilot.system.updated.casync.casync as casync - SPARSE_CHUNK_FMT = struct.Struct('H2xI4x') -CAIBX_URL = "https://commadist.azureedge.net/agnosupdate/" -AGNOS_MANIFEST_FILE = "system/hardware/tici/agnos.json" +AGNOS_MANIFEST_FILE = "openpilot/system/hardware/tici/agnos.json" class StreamingDecompressor: def __init__(self, url: str) -> None: self.buf = b"" - self.req = requests.get(url, stream=True, headers={'Accept-Encoding': None}, timeout=60) + self.req = requests.get(url, stream=True, headers={'Accept-Encoding': 'identity'}, timeout=60) self.it = self.req.iter_content(chunk_size=1024 * 1024) self.decompressor = lzma.LZMADecompressor(format=lzma.FORMAT_AUTO) self.eof = False @@ -187,50 +184,6 @@ def extract_compressed_image(target_slot_number: int, partition: dict, cloudlog) os.sync() -def extract_casync_image(target_slot_number: int, partition: dict, cloudlog): - path = get_partition_path(target_slot_number, partition) - seed_path = path[:-1] + ('b' if path[-1] == 'a' else 'a') - - target = casync.parse_caibx(partition['casync_caibx']) - - sources: list[tuple[str, casync.ChunkReader, casync.ChunkDict]] = [] - - # First source is the current partition. - try: - raw_hash = get_raw_hash(seed_path, partition['size']) - caibx_url = f"{CAIBX_URL}{partition['name']}-{raw_hash}.caibx" - - try: - cloudlog.info(f"casync fetching {caibx_url}") - sources += [('seed', casync.FileChunkReader(seed_path), casync.build_chunk_dict(casync.parse_caibx(caibx_url)))] - except requests.RequestException: - cloudlog.error(f"casync failed to load {caibx_url}") - except Exception: - cloudlog.exception("casync failed to hash seed partition") - - # Second source is the target partition, this allows for resuming - sources += [('target', casync.FileChunkReader(path), casync.build_chunk_dict(target))] - - # Finally we add the remote source to download any missing chunks - sources += [('remote', casync.RemoteChunkReader(partition['casync_store']), casync.build_chunk_dict(target))] - - last_p = 0 - - def progress(cur): - nonlocal last_p - p = int(cur / partition['size'] * 100) - if p != last_p: - last_p = p - print(f"Installing {partition['name']}: {p}", flush=True) - - stats = casync.extract(target, sources, path, progress) - cloudlog.error(f'casync done {json.dumps(stats)}') - - os.sync() - if not verify_partition(target_slot_number, partition, force_full_check=True): - raise Exception(f"Raw hash mismatch '{partition['hash_raw'].lower()}'") - - def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalone=False): cloudlog.info(f"Downloading and writing {partition['name']}") @@ -245,10 +198,7 @@ def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalo path = get_partition_path(target_slot_number, partition) - if ('casync_caibx' in partition) and not standalone: - extract_casync_image(target_slot_number, partition, cloudlog) - else: - extract_compressed_image(target_slot_number, partition, cloudlog) + extract_compressed_image(target_slot_number, partition, cloudlog) # Write hash after successful flash if not full_check: @@ -278,7 +228,7 @@ def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog, st cloudlog.info(f"Target slot {target_slot_number}") # set target slot as unbootable - os.system(f"abctl --set_unbootable {target_slot_number}") + subprocess.run(f"abctl --set_unbootable {target_slot_number}", shell=True) for partition in update: success = False diff --git a/system/hardware/tici/all-partitions.json b/openpilot/common/hardware/tici/all-partitions.json similarity index 100% rename from system/hardware/tici/all-partitions.json rename to openpilot/common/hardware/tici/all-partitions.json diff --git a/system/hardware/tici/amplifier.py b/openpilot/common/hardware/tici/amplifier.py similarity index 100% rename from system/hardware/tici/amplifier.py rename to openpilot/common/hardware/tici/amplifier.py diff --git a/system/hardware/tici/hardware.h b/openpilot/common/hardware/tici/hardware.h similarity index 86% rename from system/hardware/tici/hardware.h rename to openpilot/common/hardware/tici/hardware.h index a06ce43b35..a1a8090c35 100644 --- a/system/hardware/tici/hardware.h +++ b/openpilot/common/hardware/tici/hardware.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -8,7 +7,7 @@ #include // for std::clamp #include "common/util.h" -#include "system/hardware/base.h" +#include "common/hardware/base.h" class HardwareTici : public HardwareNone { public: @@ -22,7 +21,6 @@ public: static cereal::InitData::DeviceType get_device_type() { static const std::map device_map = { - {"tici", cereal::InitData::DeviceType::TICI}, {"tizi", cereal::InitData::DeviceType::TIZI}, {"mici", cereal::InitData::DeviceType::MICI} }; @@ -31,9 +29,6 @@ public: return it->second; } - static int get_voltage() { return std::atoi(util::read_file("/sys/class/hwmon/hwmon1/in1_input").c_str()); } - static int get_current() { return std::atoi(util::read_file("/sys/class/hwmon/hwmon1/curr1_input").c_str()); } - static std::string get_serial() { static std::string serial(""); if (serial.empty()) { @@ -54,8 +49,7 @@ public: static void set_ir_power(int percent) { auto device = get_device_type(); - if (device == cereal::InitData::DeviceType::TICI || - device == cereal::InitData::DeviceType::TIZI) { + if (device == cereal::InitData::DeviceType::TIZI) { return; } diff --git a/system/hardware/tici/hardware.py b/openpilot/common/hardware/tici/hardware.py similarity index 89% rename from system/hardware/tici/hardware.py rename to openpilot/common/hardware/tici/hardware.py index 34c426d6e7..aa125ed97d 100644 --- a/system/hardware/tici/hardware.py +++ b/openpilot/common/hardware/tici/hardware.py @@ -7,14 +7,14 @@ import time from functools import cached_property, lru_cache from pathlib import Path -from cereal import log +from openpilot.cereal import log from openpilot.common.utils import sudo_read, sudo_write from openpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action -from openpilot.system.hardware.base import HardwareBase, LPABase, ThermalConfig, ThermalZone -from openpilot.system.hardware.tici import iwlist -from openpilot.system.hardware.tici.lpa import TiciLPA -from openpilot.system.hardware.tici.pins import GPIO -from openpilot.system.hardware.tici.amplifier import Amplifier +from openpilot.common.esim.base import LPABase +from openpilot.common.hardware.base import HardwareBase, ThermalConfig, ThermalZone +from openpilot.common.esim.lpa import TiciLPA +from openpilot.common.hardware.tici.pins import GPIO +from openpilot.common.hardware.tici.amplifier import Amplifier MODEM_STATE_PATH = "/dev/shm/modem" @@ -147,9 +147,7 @@ class Tici(HardwareBase): def get_sim_lpa(self) -> LPABase: return TiciLPA() - def get_imei(self, slot): - if slot != 0: - return "" + def get_imei(self): return self.get_modem_state().get('imei', '') def get_network_info(self): @@ -233,9 +231,6 @@ class Tici(HardwareBase): return super().get_network_metered(network_type) - def get_modem_version(self): - return self.get_modem_state().get('modem_version') or None - def get_modem_temperatures(self): return self.get_modem_state().get('temperatures', []) @@ -246,7 +241,7 @@ class Tici(HardwareBase): return (self.read_param_file("/sys/class/power_supply/bms/voltage_now", int) * self.read_param_file("/sys/class/power_supply/bms/current_now", int) / 1e12) def shutdown(self): - os.system("sudo poweroff") + subprocess.run("sudo poweroff", shell=True) def get_thermal_config(self): intake, exhaust, gnss, bottomSoc = None, None, None, None @@ -313,6 +308,9 @@ class Tici(HardwareBase): continue gov = 'ondemand' if powersave_enabled else 'performance' sudo_write(gov, f'/sys/devices/system/cpu/cpufreq/policy{n}/scaling_governor') + if not powersave_enabled: + # cap max core freq to 1689 Mhz + sudo_write('1689600', f'/sys/devices/system/cpu/cpufreq/policy{n}/scaling_max_freq') # *** IRQ config *** @@ -337,11 +335,11 @@ class Tici(HardwareBase): self.amplifier.initialize_configuration() # Allow hardwared to write engagement status to kmsg - os.system("sudo chmod a+w /dev/kmsg") + subprocess.run("sudo chmod a+w /dev/kmsg", shell=True) # Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL gpio_init(GPIO.SOM_ST_IO, True) - gpio_set(GPIO.SOM_ST_IO, 1) + gpio_set(GPIO.SOM_ST_IO, True) # *** IRQ config *** @@ -380,62 +378,32 @@ class Tici(HardwareBase): pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip() subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid]) subprocess.call(["sudo", "taskset", "-pc", "3", pid]) - except subprocess.CalledProcessException as e: + except subprocess.CalledProcessError as e: print(str(e)) - def get_networks(self): - r = {} - - wlan = iwlist.scan() - if wlan is not None: - r['wlan'] = wlan - - lte_info = self.get_network_info() - if lte_info is not None: - extra = lte_info['extra'] - - # ,"LTE",,,,,,,, - # ,,,,,,, - if 'LTE' in extra: - extra = extra.split(',') - try: - r['lte'] = [{ - "mcc": int(extra[3]), - "mnc": int(extra[4]), - "cid": int(extra[5], 16), - "nmr": [{"pci": int(extra[6]), "earfcn": int(extra[7])}], - }] - except (ValueError, IndexError): - pass - - return r - def get_modem_data_usage(self): ms = self.get_modem_state() return ms.get('tx_bytes', -1), ms.get('rx_bytes', -1) - def has_internal_panda(self): - return True - def reset_internal_panda(self): gpio_init(GPIO.STM_RST_N, True) gpio_init(GPIO.STM_BOOT0, True) - gpio_set(GPIO.STM_RST_N, 1) - gpio_set(GPIO.STM_BOOT0, 0) + gpio_set(GPIO.STM_RST_N, True) + gpio_set(GPIO.STM_BOOT0, False) time.sleep(0.01) - gpio_set(GPIO.STM_RST_N, 0) + gpio_set(GPIO.STM_RST_N, False) def recover_internal_panda(self): gpio_init(GPIO.STM_RST_N, True) gpio_init(GPIO.STM_BOOT0, True) - gpio_set(GPIO.STM_RST_N, 1) - gpio_set(GPIO.STM_BOOT0, 1) + gpio_set(GPIO.STM_RST_N, True) + gpio_set(GPIO.STM_BOOT0, True) time.sleep(0.01) - gpio_set(GPIO.STM_RST_N, 0) + gpio_set(GPIO.STM_RST_N, False) time.sleep(0.01) - gpio_set(GPIO.STM_BOOT0, 0) + gpio_set(GPIO.STM_BOOT0, False) def booted(self): # this normally boots within 8s, but on rare occasions takes 30+s diff --git a/system/hardware/tici/id_rsa b/openpilot/common/hardware/tici/id_rsa similarity index 100% rename from system/hardware/tici/id_rsa rename to openpilot/common/hardware/tici/id_rsa diff --git a/system/hardware/tici/modem.py b/openpilot/common/hardware/tici/modem.py similarity index 98% rename from system/hardware/tici/modem.py rename to openpilot/common/hardware/tici/modem.py index 39266be122..54251d4815 100755 --- a/system/hardware/tici/modem.py +++ b/openpilot/common/hardware/tici/modem.py @@ -3,7 +3,6 @@ import fcntl import json import logging import os -import serial import signal import subprocess import tempfile @@ -13,6 +12,8 @@ from ipaddress import IPv4Address, AddressValueError from enum import Enum +from openpilot.common.serial import Serial + logging.basicConfig( level=logging.INFO, format="%(asctime)s.%(msecs)03d %(levelname)-7s modem: %(message)s", @@ -51,7 +52,7 @@ PPPD_CMD = [ "novj", "novjccomp", "ipcp-accept-local", "ipcp-accept-remote", "nomagic", "user", '""', "password", '""', ] -INITIAL_STATE = { +INITIAL_STATE: dict[str, object] = { "seconds_since_boot": 0, "state": "INITIALIZING", "connected": False, "ip_address": "", @@ -96,7 +97,7 @@ class PPPSession: def reset_data_port(): """Drop DTR on PPP_PORT so the modem terminates any stuck PPP session.""" try: - with serial.Serial(PPP_PORT, 460800, timeout=1) as s: + with Serial(PPP_PORT, baudrate=460800, timeout=1) as s: s.dtr = False time.sleep(0.2) s.dtr = True @@ -220,7 +221,7 @@ class Modem: os.close(fd) return [] try: - with serial.Serial(AT_PORT, 9600, timeout=5) as ser: + with Serial(AT_PORT, baudrate=9600, timeout=5) as ser: ser.reset_input_buffer() ser.write((cmd + "\r").encode()) lines = [] @@ -505,7 +506,11 @@ class Modem: except (AddressValueError, ValueError): pass if not dns_servers: - logging.warning(f"no cellular DNS servers reported by modem: {v!r}") + dns_servers = [ + "8.8.8.8", # Google + "1.1.1.1", # Cloudflare + ] + logging.warning(f"no cellular DNS servers reported by modem: {v!r}; using fallback {dns_servers}") return dns_servers def _poll_byte_counters(self) -> dict: diff --git a/system/hardware/tici/pins.py b/openpilot/common/hardware/tici/pins.py similarity index 100% rename from system/hardware/tici/pins.py rename to openpilot/common/hardware/tici/pins.py diff --git a/system/hardware/tici/power_monitor.py b/openpilot/common/hardware/tici/power_monitor.py similarity index 100% rename from system/hardware/tici/power_monitor.py rename to openpilot/common/hardware/tici/power_monitor.py diff --git a/openpilot/common/hardware/tici/tests/__init__.py b/openpilot/common/hardware/tici/tests/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/openpilot/common/hardware/tici/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/system/hardware/tici/tests/test_agnos_updater.py b/openpilot/common/hardware/tici/tests/test_agnos_updater.py similarity index 100% rename from system/hardware/tici/tests/test_agnos_updater.py rename to openpilot/common/hardware/tici/tests/test_agnos_updater.py diff --git a/system/hardware/tici/tests/test_amplifier.py b/openpilot/common/hardware/tici/tests/test_amplifier.py similarity index 90% rename from system/hardware/tici/tests/test_amplifier.py rename to openpilot/common/hardware/tici/tests/test_amplifier.py index 9ce00c3ff2..3e36ff3a3d 100644 --- a/system/hardware/tici/tests/test_amplifier.py +++ b/openpilot/common/hardware/tici/tests/test_amplifier.py @@ -1,11 +1,10 @@ import pytest import time -import random import subprocess from panda import Panda -from openpilot.system.hardware import TICI, HARDWARE -from openpilot.system.hardware.tici.amplifier import Amplifier +from openpilot.common.hardware import TICI, HARDWARE +from openpilot.common.hardware.tici.amplifier import Amplifier class TestAmplifier: @@ -57,7 +56,7 @@ class TestAmplifier: time.sleep(0.1) self.panda.set_siren(True) - time.sleep(random.randint(0, 5)) + time.sleep(0.1) amp = Amplifier(debug=True) r = amp.initialize_configuration() diff --git a/system/hardware/tici/updater b/openpilot/common/hardware/tici/updater similarity index 100% rename from system/hardware/tici/updater rename to openpilot/common/hardware/tici/updater diff --git a/openpilot/common/hardware/usb.py b/openpilot/common/hardware/usb.py new file mode 100644 index 0000000000..983002b538 --- /dev/null +++ b/openpilot/common/hardware/usb.py @@ -0,0 +1,73 @@ +from pathlib import Path + +CHESTNUT_VENDOR_ID = 0xADD1 +CHESTNUT_PRODUCT_ID = 0x0001 +USB_DEVICES_PATH = Path("/sys/bus/usb/devices") + + +def read(path: Path) -> str | None: + try: + return path.read_text().strip() + except OSError: + return None + + +def read_int(path: Path, base: int = 10) -> int: + try: + return int(path.read_text(), base) + except (OSError, ValueError): + return 0 + + +def usb_devices() -> list[Path]: + try: + devices = (d for d in USB_DEVICES_PATH.glob("*") if (d / "idVendor").exists()) + return sorted(devices, key=lambda p: p.name) + except OSError: + return [] + + +def controller(device: Path) -> Path | None: + try: + return next((parent for parent in device.resolve().parents if parent.name.endswith(".ssusb")), None) + except OSError: + return None + + +def get_usb_state() -> list[dict]: + devices = [] + for device in usb_devices(): + vendor_id = read_int(device / "idVendor", 16) + product_id = read_int(device / "idProduct", 16) + ctrl = controller(device) + devices.append({ + "busnum": read_int(device / "busnum"), + "devnum": read_int(device / "devnum"), + "vendorId": vendor_id, + "productId": product_id, + "speedMbps": read_int(device / "speed"), + "manufacturer": read(device / "manufacturer") or "", + "product": read(device / "product") or "", + "linkErrorCount": read_int(ctrl / "portli", 0) & 0xFFFF if ctrl is not None else 0, + }) + return devices + + +def set_usb_state(device_state, devices: list[dict]) -> None: + entries = device_state.usbState.init('devices', len(devices)) + + chestnut_present = False + for entry, device in zip(entries, devices, strict=True): + entry.busnum = device["busnum"] + entry.devnum = device["devnum"] + entry.vendorId = device["vendorId"] + entry.productId = device["productId"] + entry.speedMbps = device["speedMbps"] + entry.manufacturer = device["manufacturer"] + entry.product = device["product"] + entry.linkErrorCount = device["linkErrorCount"] + + if (entry.vendorId, entry.productId) == (CHESTNUT_VENDOR_ID, CHESTNUT_PRODUCT_ID): + chestnut_present = True + + device_state.chestnutPresent = chestnut_present diff --git a/common/i2c.py b/openpilot/common/i2c.py similarity index 100% rename from common/i2c.py rename to openpilot/common/i2c.py diff --git a/openpilot/common/linux.py b/openpilot/common/linux.py new file mode 100644 index 0000000000..73ea77b509 --- /dev/null +++ b/openpilot/common/linux.py @@ -0,0 +1,50 @@ +class LinuxSystemStats: + def __init__(self) -> None: + self._last_cpu_times = self._read_cpu_times() + + @staticmethod + def _read_cpu_times() -> dict[int, tuple[int, int]]: + cpu_times = {} + with open('/proc/stat') as f: + for line in f: + name, *values = line.split() + if not name.startswith('cpu') or not name[3:].isdigit(): + continue + + times = [int(value) for value in values] + idle = sum(times[3:5]) + total = sum(times[:8]) + cpu_times[int(name[3:])] = (idle, total) + return cpu_times + + def cpu_usage_percent(self) -> list[float]: + current_cpu_times = self._read_cpu_times() + usage = [] + for cpu, (idle, total) in sorted(current_cpu_times.items()): + last_times = self._last_cpu_times.get(cpu) + if last_times is None: + usage.append(0.) + continue + + last_idle, last_total = last_times + idle_delta = idle - last_idle + total_delta = total - last_total + if idle_delta < 0 or total_delta <= 0: + usage.append(0.) + else: + usage.append(max(0., min(100., 100. * (total_delta - idle_delta) / total_delta))) + + self._last_cpu_times = current_cpu_times + return usage + + @staticmethod + def memory_usage_percent() -> float: + memory = {} + with open('/proc/meminfo') as f: + for line in f: + key, value, *_ = line.split() + if key in ('MemTotal:', 'MemAvailable:'): + memory[key] = int(value) + + total = memory['MemTotal:'] + return max(0., min(100., 100. * (total - memory['MemAvailable:']) / total)) diff --git a/common/logging_extra.py b/openpilot/common/logging_extra.py similarity index 100% rename from common/logging_extra.py rename to openpilot/common/logging_extra.py diff --git a/common/markdown.py b/openpilot/common/markdown.py similarity index 100% rename from common/markdown.py rename to openpilot/common/markdown.py diff --git a/common/mock/__init__.py b/openpilot/common/mock/__init__.py similarity index 88% rename from common/mock/__init__.py rename to openpilot/common/mock/__init__.py index 4b01dfe841..ff4dd32b92 100644 --- a/common/mock/__init__.py +++ b/openpilot/common/mock/__init__.py @@ -1,13 +1,13 @@ """ Utilities for generating mock messages for testing. -example in common/tests/test_mock.py +example in openpilot/common/tests/test_mock.py """ import functools import threading -from cereal.messaging import PubMaster -from cereal.services import SERVICE_LIST +from openpilot.cereal.messaging import PubMaster +from openpilot.cereal.services import SERVICE_LIST from openpilot.common.mock.generators import generate_livePose from openpilot.common.realtime import Ratekeeper diff --git a/common/mock/generators.py b/openpilot/common/mock/generators.py similarity index 91% rename from common/mock/generators.py rename to openpilot/common/mock/generators.py index 5cd9c88a56..28a3b98e58 100644 --- a/common/mock/generators.py +++ b/openpilot/common/mock/generators.py @@ -1,4 +1,4 @@ -from cereal import messaging +from openpilot.cereal import messaging def generate_livePose(): diff --git a/common/parameterized.py b/openpilot/common/parameterized.py similarity index 100% rename from common/parameterized.py rename to openpilot/common/parameterized.py diff --git a/common/params.cc b/openpilot/common/params.cc similarity index 99% rename from common/params.cc rename to openpilot/common/params.cc index 39592cb905..bed6827e5a 100644 --- a/common/params.cc +++ b/openpilot/common/params.cc @@ -12,7 +12,7 @@ #include "common/queue.h" #include "common/swaglog.h" #include "common/util.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" namespace { diff --git a/common/params.h b/openpilot/common/params.h similarity index 100% rename from common/params.h rename to openpilot/common/params.h diff --git a/common/params.py b/openpilot/common/params.py similarity index 100% rename from common/params.py rename to openpilot/common/params.py diff --git a/common/params_keys.h b/openpilot/common/params_keys.h similarity index 98% rename from common/params_keys.h rename to openpilot/common/params_keys.h index 5cb052ef0f..3c39fc5f53 100644 --- a/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -3,7 +3,7 @@ #include #include -#include "cereal/gen/cpp/log.capnp.h" +#include "openpilot/cereal/gen/cpp/log.capnp.h" inline static std::unordered_map keys = { {"AccessToken", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}}, @@ -37,6 +37,7 @@ inline static std::unordered_map keys = { {"DoShutdown", {CLEAR_ON_MANAGER_START, BOOL}}, {"DoUninstall", {CLEAR_ON_MANAGER_START, BOOL}}, {"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}}, + {"DriverLockoutCount", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, INT, "0"}}, {"AlphaLongitudinalEnabled", {PERSISTENT | DEVELOPMENT_ONLY | BACKUP, BOOL}}, {"ExperimentalMode", {PERSISTENT | BACKUP, BOOL}}, {"ExperimentalModeConfirmed", {PERSISTENT | BACKUP, BOOL}}, @@ -58,12 +59,11 @@ inline static std::unordered_map keys = { {"IsDriverViewEnabled", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsEngaged", {PERSISTENT, BOOL}}, {"IsLdwEnabled", {PERSISTENT | BACKUP, BOOL}}, + {"IsLiveStreaming", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsMetric", {PERSISTENT | BACKUP, BOOL}}, {"IsOffroad", {CLEAR_ON_MANAGER_START, BOOL}}, - {"IsOnroad", {PERSISTENT, BOOL}}, {"IsRhdDetected", {PERSISTENT, BOOL}}, {"IsReleaseBranch", {CLEAR_ON_MANAGER_START, BOOL}}, - {"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsTestedBranch", {CLEAR_ON_MANAGER_START, BOOL}}, {"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"LanguageSetting", {PERSISTENT | BACKUP, STRING, "en"}}, @@ -81,6 +81,7 @@ inline static std::unordered_map keys = { {"LiveParameters", {PERSISTENT, JSON}}, {"LiveParametersV2", {PERSISTENT, BYTES}}, {"LivestreamEncoderBitrate", {CLEAR_ON_MANAGER_START | DONT_LOG, INT}}, + {"LivestreamRequestKeyframe", {CLEAR_ON_MANAGER_START | DONT_LOG, BOOL}}, {"LiveTorqueParameters", {PERSISTENT | DONT_LOG, BYTES}}, {"LocationFilterInitialState", {PERSISTENT, BYTES}}, {"LateralManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, @@ -93,7 +94,6 @@ inline static std::unordered_map keys = { {"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ExcessiveActuation", {PERSISTENT, JSON}}, - {"Offroad_IsTakingSnapshot", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_NeosUpdate", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_NoFirmware", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_Recalibration", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, diff --git a/common/params_pyx.pyx b/openpilot/common/params_pyx.pyx similarity index 100% rename from common/params_pyx.pyx rename to openpilot/common/params_pyx.pyx diff --git a/common/pid.py b/openpilot/common/pid.py similarity index 76% rename from common/pid.py rename to openpilot/common/pid.py index b3d64d6fcd..f541baf05c 100644 --- a/common/pid.py +++ b/openpilot/common/pid.py @@ -1,11 +1,13 @@ import numpy as np -from numbers import Number +from collections.abc import Sequence + +Gain = int | float | tuple[Sequence[float], Sequence[float]] | list[list[float]] class PIDController: - def __init__(self, k_p, k_i, k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100): - self._k_p: list[list[float]] = [[0], [k_p]] if isinstance(k_p, Number) else k_p - self._k_i: list[list[float]] = [[0], [k_i]] if isinstance(k_i, Number) else k_i - self._k_d: list[list[float]] = [[0], [k_d]] if isinstance(k_d, Number) else k_d + def __init__(self, k_p: Gain, k_i: Gain, k_d: Gain = 0., pos_limit=1e308, neg_limit=-1e308, rate=100): + self._k_p = ([0], [k_p]) if isinstance(k_p, (int, float)) else k_p + self._k_i = ([0], [k_i]) if isinstance(k_i, (int, float)) else k_i + self._k_d = ([0], [k_d]) if isinstance(k_d, (int, float)) else k_d self.set_limits(pos_limit, neg_limit) diff --git a/common/prefix.h b/openpilot/common/prefix.h similarity index 97% rename from common/prefix.h rename to openpilot/common/prefix.h index de3a94d71f..89f346b9e5 100644 --- a/common/prefix.h +++ b/openpilot/common/prefix.h @@ -5,7 +5,7 @@ #include "common/params.h" #include "common/util.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" class OpenpilotPrefix { public: diff --git a/common/prefix.py b/openpilot/common/prefix.py similarity index 92% rename from common/prefix.py rename to openpilot/common/prefix.py index d0a5f92628..d0be8997ae 100644 --- a/common/prefix.py +++ b/openpilot/common/prefix.py @@ -5,9 +5,9 @@ import uuid from openpilot.common.params import Params -from openpilot.system.hardware import PC -from openpilot.system.hardware.hw import Paths -from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT +from openpilot.common.hardware import PC +from openpilot.common.hardware.hw import Paths +from openpilot.common.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT class OpenpilotPrefix: def __init__(self, prefix: str | None = None, create_dirs_on_enter: bool = True, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): diff --git a/common/queue.h b/openpilot/common/queue.h similarity index 100% rename from common/queue.h rename to openpilot/common/queue.h diff --git a/common/ratekeeper.cc b/openpilot/common/ratekeeper.cc similarity index 100% rename from common/ratekeeper.cc rename to openpilot/common/ratekeeper.cc diff --git a/common/ratekeeper.h b/openpilot/common/ratekeeper.h similarity index 100% rename from common/ratekeeper.h rename to openpilot/common/ratekeeper.h diff --git a/common/realtime.py b/openpilot/common/realtime.py similarity index 98% rename from common/realtime.py rename to openpilot/common/realtime.py index 91d2e165e9..e0b9e8a3a5 100644 --- a/common/realtime.py +++ b/openpilot/common/realtime.py @@ -7,7 +7,7 @@ import time from setproctitle import getproctitle from openpilot.common.utils import MovingAverage -from openpilot.system.hardware import PC +from openpilot.common.hardware import PC # time step for each process diff --git a/openpilot/common/serial.py b/openpilot/common/serial.py new file mode 100644 index 0000000000..68083f40a6 --- /dev/null +++ b/openpilot/common/serial.py @@ -0,0 +1,272 @@ +import errno +import fcntl +import os +import select +import struct +import termios +import time + + +# Modem control lines (linux/termios.h); fall back to common x86_64 values. +TIOCMBIS = getattr(termios, "TIOCMBIS", 0x5416) +TIOCMBIC = getattr(termios, "TIOCMBIC", 0x5417) +TIOCM_DTR = getattr(termios, "TIOCM_DTR", 0x002) +TIOCM_RTS = getattr(termios, "TIOCM_RTS", 0x004) +_TIOCM_DTR = struct.pack("I", TIOCM_DTR) +_TIOCM_RTS = struct.pack("I", TIOCM_RTS) + + +class SerialException(OSError): + pass + + +class Serial: + def __init__(self, port: str, baudrate: int = 9600, timeout: float | None = None, *, + rtscts: bool = False, dsrdtr: bool = False, exclusive: bool = False): + self._port = port + self._baudrate = baudrate + self._timeout = timeout + self._rtscts = rtscts + self._dsrdtr = dsrdtr + self._exclusive = exclusive + self._dtr = True + self._fd = -1 + self.open() + + def __enter__(self): + return self + + def __exit__(self, *args) -> None: + self.close() + + @property + def fd(self) -> int: + self._ensure_open() + return self._fd + + @property + def baudrate(self) -> int: + return self._baudrate + + @baudrate.setter + def baudrate(self, value: int) -> None: + self._baudrate = int(value) + if self._fd >= 0: + self._configure() + + @property + def dtr(self) -> bool: + return self._dtr + + @dtr.setter + def dtr(self, value: bool) -> None: + self._dtr = bool(value) + if self._fd >= 0: + self._set_line(TIOCM_DTR, _TIOCM_DTR, self._dtr) + + def open(self) -> None: + if self._fd >= 0: + return + try: + self._fd = os.open(self._port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK) + except OSError as e: + self._fd = -1 + raise SerialException(e.errno, f"could not open port {self._port}: {e}") from e + + try: + if self._exclusive: + try: + fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError as e: + raise SerialException(e.errno, f"could not exclusively lock port {self._port}: {e}") from e + + self._configure() + + # When not using hardware DSR/DTR handshaking, drive lines ourselves. + if not self._dsrdtr: + try: + self._set_line(TIOCM_DTR, _TIOCM_DTR, self._dtr) + if not self._rtscts: + self._set_line(TIOCM_RTS, _TIOCM_RTS, True) + except OSError as e: + if e.errno not in (errno.EINVAL, errno.ENOTTY): + raise + + self.reset_input_buffer() + except BaseException: + self._close_fd() + raise + + def close(self) -> None: + self._close_fd() + + def read(self, size: int = 1) -> bytes: + self._ensure_open() + if size <= 0: + return b"" + + buf = bytearray() + deadline = self._deadline() + while len(buf) < size: + remaining = None if deadline is None else max(0.0, deadline - time.monotonic()) + if not self._wait_readable(remaining): + break + try: + chunk = os.read(self._fd, size - len(buf)) + except InterruptedError: + continue + except OSError as e: + if e.errno in (errno.EAGAIN, errno.EWOULDBLOCK): + if self._timeout == 0: + break + continue + raise SerialException(e.errno, f"read failed: {e}") from e + if not chunk: + break + buf.extend(chunk) + return bytes(buf) + + def readline(self) -> bytes: + self._ensure_open() + buf = bytearray() + deadline = self._deadline() + while True: + remaining = None if deadline is None else max(0.0, deadline - time.monotonic()) + if deadline is not None and remaining == 0.0 and not buf: + # match pyserial: timed-out readline returns empty + if not self._wait_readable(0.0): + return b"" + elif not self._wait_readable(remaining): + return bytes(buf) + + try: + chunk = os.read(self._fd, 1) + except InterruptedError: + continue + except OSError as e: + if e.errno in (errno.EAGAIN, errno.EWOULDBLOCK): + if self._timeout == 0: + return bytes(buf) + continue + raise SerialException(e.errno, f"read failed: {e}") from e + if not chunk: + return bytes(buf) + buf.extend(chunk) + if chunk == b"\n": + return bytes(buf) + + def write(self, data: bytes) -> int: + self._ensure_open() + if not data: + return 0 + view = memoryview(data) + total = 0 + while total < len(data): + try: + n = os.write(self._fd, view[total:]) + except InterruptedError: + continue + except OSError as e: + if e.errno in (errno.EAGAIN, errno.EWOULDBLOCK): + select.select([], [self._fd], [], None) + continue + raise SerialException(e.errno, f"write failed: {e}") from e + if n == 0: + raise SerialException("write returned 0") + total += n + return total + + def flush(self) -> None: + self._ensure_open() + termios.tcdrain(self._fd) + + def reset_input_buffer(self) -> None: + self._ensure_open() + termios.tcflush(self._fd, termios.TCIFLUSH) + + def reset_output_buffer(self) -> None: + self._ensure_open() + termios.tcflush(self._fd, termios.TCOFLUSH) + + def _close_fd(self) -> None: + if self._fd >= 0: + try: + if self._exclusive: + fcntl.flock(self._fd, fcntl.LOCK_UN) + except OSError: + pass + try: + os.close(self._fd) + except OSError: + pass + self._fd = -1 + + def _ensure_open(self) -> None: + if self._fd < 0: + raise SerialException("port is not open") + + def _deadline(self) -> float | None: + if self._timeout is None: + return None + if self._timeout == 0: + return time.monotonic() + return time.monotonic() + self._timeout + + def _wait_readable(self, timeout: float | None) -> bool: + """Return True if fd is readable. timeout None blocks; 0 polls.""" + if timeout is not None and timeout < 0: + timeout = 0.0 + try: + ready, _, _ = select.select([self._fd], [], [], timeout) + except InterruptedError: + return False + return bool(ready) + + def _baud_constant(self, baudrate: int) -> int: + try: + return getattr(termios, f"B{baudrate}") + except AttributeError as e: + raise ValueError(f"unsupported baud rate: {baudrate}") from e + + def _configure(self) -> None: + self._ensure_open() + try: + attrs = termios.tcgetattr(self._fd) + except termios.error as e: + raise SerialException(f"could not get port attributes: {e}") from e + + iflag, oflag, cflag, lflag, _ispeed, _ospeed, cc = attrs + + # raw binary 8N1 + iflag = 0 + oflag = 0 + lflag = 0 + cflag |= termios.CLOCAL | termios.CREAD + cflag &= ~termios.CSIZE + cflag |= termios.CS8 + cflag &= ~(termios.PARENB | termios.PARODD | termios.CSTOPB) + + if hasattr(termios, "CRTSCTS"): + if self._rtscts: + cflag |= termios.CRTSCTS + else: + cflag &= ~termios.CRTSCTS + + speed = self._baud_constant(self._baudrate) + cc = list(cc) + # Non-blocking reads are handled via select + O_NONBLOCK; keep VMIN/VTIME at 0. + cc[termios.VMIN] = 0 + cc[termios.VTIME] = 0 + + try: + termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, speed, speed, cc]) + except termios.error as e: + raise SerialException(f"could not configure port: {e}") from e + + # Keep the fd non-blocking so timeout=0 and select work consistently. + flags = fcntl.fcntl(self._fd, fcntl.F_GETFL) + fcntl.fcntl(self._fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) + + def _set_line(self, _bit: int, packed: bytes, enabled: bool) -> None: + request = TIOCMBIS if enabled else TIOCMBIC + fcntl.ioctl(self._fd, request, packed) diff --git a/common/simple_kalman.py b/openpilot/common/simple_kalman.py similarity index 100% rename from common/simple_kalman.py rename to openpilot/common/simple_kalman.py diff --git a/common/spinner.py b/openpilot/common/spinner.py similarity index 97% rename from common/spinner.py rename to openpilot/common/spinner.py index 12a816eaf8..f152285138 100755 --- a/common/spinner.py +++ b/openpilot/common/spinner.py @@ -8,7 +8,7 @@ class Spinner: try: self.spinner_proc = subprocess.Popen(["./spinner.py"], stdin=subprocess.PIPE, - cwd=os.path.join(BASEDIR, "system", "ui"), + cwd=os.path.join(BASEDIR, "openpilot/system", "ui"), close_fds=True) except OSError: self.spinner_proc = None diff --git a/common/stat_live.py b/openpilot/common/stat_live.py similarity index 100% rename from common/stat_live.py rename to openpilot/common/stat_live.py diff --git a/common/swaglog.cc b/openpilot/common/swaglog.cc similarity index 99% rename from common/swaglog.cc rename to openpilot/common/swaglog.cc index 8811d61d8f..9834098929 100644 --- a/common/swaglog.cc +++ b/openpilot/common/swaglog.cc @@ -13,7 +13,7 @@ #include #include "json11/json11.hpp" #include "common/version.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include "sunnypilot/common/version.h" diff --git a/common/swaglog.h b/openpilot/common/swaglog.h similarity index 100% rename from common/swaglog.h rename to openpilot/common/swaglog.h diff --git a/common/swaglog.py b/openpilot/common/swaglog.py similarity index 97% rename from common/swaglog.py rename to openpilot/common/swaglog.py index d009f00e76..8b629b3fba 100644 --- a/common/swaglog.py +++ b/openpilot/common/swaglog.py @@ -8,7 +8,7 @@ from logging.handlers import BaseRotatingHandler import zmq from openpilot.common.logging_extra import SwagLogger, SwagFormatter, SwagLogFileFormatter -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths def get_file_handler(): @@ -27,7 +27,7 @@ class SwaglogRotatingFileHandler(BaseRotatingHandler): self.log_files = self.get_existing_logfiles() log_indexes = [f.split(".")[-1] for f in self.log_files] self.last_file_idx = max([int(i) for i in log_indexes if i.isdigit()] or [-1]) - self.last_rollover = None + self.last_rollover = 0.0 self.doRollover() def _open(self): @@ -39,7 +39,7 @@ class SwaglogRotatingFileHandler(BaseRotatingHandler): return stream def get_existing_logfiles(self): - log_files = list() + log_files = [] base_dir = os.path.dirname(self.base_filename) for fn in os.listdir(base_dir): fp = os.path.join(base_dir, fn) diff --git a/common/tests/.gitignore b/openpilot/common/tests/.gitignore similarity index 100% rename from common/tests/.gitignore rename to openpilot/common/tests/.gitignore diff --git a/common/transformations/__init__.py b/openpilot/common/tests/__init__.py similarity index 100% rename from common/transformations/__init__.py rename to openpilot/common/tests/__init__.py diff --git a/common/tests/test_file_helpers.py b/openpilot/common/tests/test_file_helpers.py similarity index 100% rename from common/tests/test_file_helpers.py rename to openpilot/common/tests/test_file_helpers.py diff --git a/common/tests/test_markdown.py b/openpilot/common/tests/test_markdown.py similarity index 100% rename from common/tests/test_markdown.py rename to openpilot/common/tests/test_markdown.py diff --git a/common/tests/test_params.py b/openpilot/common/tests/test_params.py similarity index 96% rename from common/tests/test_params.py rename to openpilot/common/tests/test_params.py index fcb8e5a185..bbd411bc37 100644 --- a/common/tests/test_params.py +++ b/openpilot/common/tests/test_params.py @@ -112,14 +112,14 @@ class TestParams: def test_params_default_value(self): self.params.remove("LanguageSetting") self.params.remove("LongitudinalPersonality") - self.params.remove("LiveParameters") + self.params.remove("LiveParametersV2") assert self.params.get("LanguageSetting") is None assert self.params.get("LanguageSetting", return_default=False) is None assert isinstance(self.params.get("LanguageSetting", return_default=True), str) assert isinstance(self.params.get("LongitudinalPersonality", return_default=True), int) - assert self.params.get("LiveParameters") is None - assert self.params.get("LiveParameters", return_default=True) is None + assert self.params.get("LiveParametersV2") is None + assert self.params.get("LiveParametersV2", return_default=True) is None def test_params_get_type(self): # json diff --git a/common/tests/test_runner.cc b/openpilot/common/tests/test_runner.cc similarity index 100% rename from common/tests/test_runner.cc rename to openpilot/common/tests/test_runner.cc diff --git a/common/tests/test_simple_kalman.py b/openpilot/common/tests/test_simple_kalman.py similarity index 100% rename from common/tests/test_simple_kalman.py rename to openpilot/common/tests/test_simple_kalman.py diff --git a/common/tests/test_swaglog.cc b/openpilot/common/tests/test_swaglog.cc similarity index 98% rename from common/tests/test_swaglog.cc rename to openpilot/common/tests/test_swaglog.cc index 26fd33436b..9a76b78fd5 100644 --- a/common/tests/test_swaglog.cc +++ b/openpilot/common/tests/test_swaglog.cc @@ -6,7 +6,7 @@ #include "common/swaglog.h" #include "common/util.h" #include "common/version.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include "json11/json11.hpp" #include "sunnypilot/common/version.h" diff --git a/common/tests/test_util.cc b/openpilot/common/tests/test_util.cc similarity index 100% rename from common/tests/test_util.cc rename to openpilot/common/tests/test_util.cc diff --git a/common/text_window.py b/openpilot/common/text_window.py similarity index 98% rename from common/text_window.py rename to openpilot/common/text_window.py index 358243d1f1..94cfa7811e 100755 --- a/common/text_window.py +++ b/openpilot/common/text_window.py @@ -10,7 +10,7 @@ class TextWindow: try: self.text_proc = subprocess.Popen(["./text.py", text], stdin=subprocess.PIPE, - cwd=os.path.join(BASEDIR, "system", "ui"), + cwd=os.path.join(BASEDIR, "openpilot/system", "ui"), close_fds=True) except OSError: self.text_proc = None diff --git a/common/time_helpers.py b/openpilot/common/time_helpers.py similarity index 100% rename from common/time_helpers.py rename to openpilot/common/time_helpers.py diff --git a/common/timeout.py b/openpilot/common/timeout.py similarity index 100% rename from common/timeout.py rename to openpilot/common/timeout.py diff --git a/common/timing.h b/openpilot/common/timing.h similarity index 100% rename from common/timing.h rename to openpilot/common/timing.h diff --git a/common/transformations/README.md b/openpilot/common/transformations/README.md similarity index 100% rename from common/transformations/README.md rename to openpilot/common/transformations/README.md diff --git a/common/transformations/tests/__init__.py b/openpilot/common/transformations/__init__.py similarity index 100% rename from common/transformations/tests/__init__.py rename to openpilot/common/transformations/__init__.py diff --git a/common/transformations/camera.py b/openpilot/common/transformations/camera.py similarity index 98% rename from common/transformations/camera.py rename to openpilot/common/transformations/camera.py index 2e68b5e37c..ada9c5b398 100644 --- a/common/transformations/camera.py +++ b/openpilot/common/transformations/camera.py @@ -52,7 +52,7 @@ _ar_ox_config = DeviceCameraConfig(CameraConfig(1928, 1208, 2648.0), _ar_ox_fish _os_config = DeviceCameraConfig(CameraConfig(2688 // 2, 1520 // 2, 1522.0 * 3 / 4), _os_fisheye, _os_fisheye) _neo_config = DeviceCameraConfig(CameraConfig(1164, 874, 910.0), CameraConfig(816, 612, 650.0), _NoneCameraConfig()) -DEVICE_CAMERAS = { +DEVICE_CAMERAS: dict[tuple[str, str], DeviceCameraConfig] = { # A "device camera" is defined by a device type and sensor # sensor type was never set on eon/neo/two @@ -176,4 +176,3 @@ def img_from_device(pt_device): pt_img = pt_view/pt_view[:, 2:3] return pt_img.reshape(input_shape)[:, :2] - diff --git a/common/transformations/coordinates.py b/openpilot/common/transformations/coordinates.py similarity index 100% rename from common/transformations/coordinates.py rename to openpilot/common/transformations/coordinates.py diff --git a/common/transformations/model.py b/openpilot/common/transformations/model.py similarity index 100% rename from common/transformations/model.py rename to openpilot/common/transformations/model.py diff --git a/common/transformations/orientation.py b/openpilot/common/transformations/orientation.py similarity index 95% rename from common/transformations/orientation.py rename to openpilot/common/transformations/orientation.py index 86e6a6c347..d2ccda13a5 100644 --- a/common/transformations/orientation.py +++ b/openpilot/common/transformations/orientation.py @@ -25,11 +25,10 @@ def numpy_wrap(function, input_shape, output_shape) -> Callable[..., np.ndarray] # Add empty dimension if inputs is not a list if len(shape) == len(input_shape): - inp.shape = (1, ) + inp.shape + inp = inp.reshape((1, ) + inp.shape) result = np.asarray([function(*args, i) for i in inp]) - result.shape = out_shape - return result + return result.reshape(out_shape) return f diff --git a/selfdrive/__init__.py b/openpilot/common/transformations/tests/__init__.py similarity index 100% rename from selfdrive/__init__.py rename to openpilot/common/transformations/tests/__init__.py diff --git a/common/transformations/tests/test_coordinates.py b/openpilot/common/transformations/tests/test_coordinates.py similarity index 100% rename from common/transformations/tests/test_coordinates.py rename to openpilot/common/transformations/tests/test_coordinates.py diff --git a/common/transformations/tests/test_orientation.py b/openpilot/common/transformations/tests/test_orientation.py similarity index 100% rename from common/transformations/tests/test_orientation.py rename to openpilot/common/transformations/tests/test_orientation.py diff --git a/common/transformations/transformations.py b/openpilot/common/transformations/transformations.py similarity index 100% rename from common/transformations/transformations.py rename to openpilot/common/transformations/transformations.py diff --git a/common/util.cc b/openpilot/common/util.cc similarity index 100% rename from common/util.cc rename to openpilot/common/util.cc diff --git a/common/util.h b/openpilot/common/util.h similarity index 100% rename from common/util.h rename to openpilot/common/util.h diff --git a/common/utils.py b/openpilot/common/utils.py similarity index 98% rename from common/utils.py rename to openpilot/common/utils.py index 183363f39d..22d024fca7 100644 --- a/common/utils.py +++ b/openpilot/common/utils.py @@ -37,13 +37,13 @@ def sudo_write(val: str, path: str) -> None: with open(path, 'w') as f: f.write(str(val)) except PermissionError: - os.system(f"sudo chmod a+w {path}") + subprocess.run(f"sudo chmod a+w {path}", shell=True) try: with open(path, 'w') as f: f.write(str(val)) except PermissionError: # fallback for debugfs files - os.system(f"sudo su -c 'echo {val} > {path}'") + subprocess.run(f"sudo su -c 'echo {val} > {path}'", shell=True) def sudo_read(path: str) -> str: diff --git a/common/version.h b/openpilot/common/version.h similarity index 100% rename from common/version.h rename to openpilot/common/version.h diff --git a/system/version.py b/openpilot/common/version.py similarity index 98% rename from system/version.py rename to openpilot/common/version.py index 2d4fc8f765..f1514aa3cd 100755 --- a/system/version.py +++ b/openpilot/common/version.py @@ -40,7 +40,7 @@ sunnylink_consent_declined: str = "-1" def get_version(path: str = BASEDIR) -> str: - with open(os.path.join(path, "sunnypilot", "common", "version.h")) as _versionf: + with open(os.path.join(path, "openpilot", "sunnypilot", "common", "version.h")) as _versionf: version = _versionf.read().split('"')[1] return version diff --git a/openpilot/common/yuv.cc b/openpilot/common/yuv.cc new file mode 100644 index 0000000000..a7a64fb594 --- /dev/null +++ b/openpilot/common/yuv.cc @@ -0,0 +1,132 @@ +#include "common/yuv.h" + +#include +#include + +namespace yuv { + +namespace { + +inline uint8_t clamp_u8(int v) { + return static_cast(std::clamp(v, 0, 255)); +} + +void copy_plane(const uint8_t *src, int src_stride, + uint8_t *dst, int dst_stride, + int width, int height) { + if (src_stride == width && dst_stride == width) { + std::memcpy(dst, src, static_cast(width) * height); + return; + } + for (int y = 0; y < height; ++y) { + std::memcpy(dst + y * dst_stride, src + y * src_stride, width); + } +} + +void scale_plane_point(const uint8_t *src, int src_stride, int src_width, int src_height, + uint8_t *dst, int dst_stride, int dst_width, int dst_height) { + if (src_width == dst_width && src_height == dst_height) { + copy_plane(src, src_stride, dst, dst_stride, dst_width, dst_height); + return; + } + for (int y = 0; y < dst_height; ++y) { + const int sy = y * src_height / dst_height; + const uint8_t *src_row = src + sy * src_stride; + uint8_t *dst_row = dst + y * dst_stride; + for (int x = 0; x < dst_width; ++x) { + dst_row[x] = src_row[x * src_width / dst_width]; + } + } +} + +// BT.601 limited range → RGB (integer form used widely, incl. similar to libyuv). +inline void yuv_to_rgb(int y, int u, int v, uint8_t *r, uint8_t *g, uint8_t *b) { + const int c = (y - 16) * 298; + const int d = u - 128; + const int e = v - 128; + *r = clamp_u8((c + 409 * e + 128) >> 8); + *g = clamp_u8((c - 100 * d - 208 * e + 128) >> 8); + *b = clamp_u8((c + 516 * d + 128) >> 8); +} + +} // namespace + +void nv12_to_i420(const uint8_t *src_y, int src_stride_y, + const uint8_t *src_uv, int src_stride_uv, + uint8_t *dst_y, int dst_stride_y, + uint8_t *dst_u, int dst_stride_u, + uint8_t *dst_v, int dst_stride_v, + int width, int height) { + copy_plane(src_y, src_stride_y, dst_y, dst_stride_y, width, height); + + const int uv_width = width / 2; + const int uv_height = height / 2; + for (int y = 0; y < uv_height; ++y) { + const uint8_t *uv = src_uv + y * src_stride_uv; + uint8_t *u = dst_u + y * dst_stride_u; + uint8_t *v = dst_v + y * dst_stride_v; + for (int x = 0; x < uv_width; ++x) { + u[x] = uv[2 * x]; + v[x] = uv[2 * x + 1]; + } + } +} + +void i420_to_nv12(const uint8_t *src_y, int src_stride_y, + const uint8_t *src_u, int src_stride_u, + const uint8_t *src_v, int src_stride_v, + uint8_t *dst_y, int dst_stride_y, + uint8_t *dst_uv, int dst_stride_uv, + int width, int height) { + copy_plane(src_y, src_stride_y, dst_y, dst_stride_y, width, height); + + const int uv_width = width / 2; + const int uv_height = height / 2; + for (int y = 0; y < uv_height; ++y) { + const uint8_t *u = src_u + y * src_stride_u; + const uint8_t *v = src_v + y * src_stride_v; + uint8_t *uv = dst_uv + y * dst_stride_uv; + for (int x = 0; x < uv_width; ++x) { + uv[2 * x] = u[x]; + uv[2 * x + 1] = v[x]; + } + } +} + +void i420_scale(const uint8_t *src_y, int src_stride_y, + const uint8_t *src_u, int src_stride_u, + const uint8_t *src_v, int src_stride_v, + int src_width, int src_height, + uint8_t *dst_y, int dst_stride_y, + uint8_t *dst_u, int dst_stride_u, + uint8_t *dst_v, int dst_stride_v, + int dst_width, int dst_height) { + scale_plane_point(src_y, src_stride_y, src_width, src_height, + dst_y, dst_stride_y, dst_width, dst_height); + scale_plane_point(src_u, src_stride_u, src_width / 2, src_height / 2, + dst_u, dst_stride_u, dst_width / 2, dst_height / 2); + scale_plane_point(src_v, src_stride_v, src_width / 2, src_height / 2, + dst_v, dst_stride_v, dst_width / 2, dst_height / 2); +} + +void nv12_to_rgba(const uint8_t *src_y, int src_stride_y, + const uint8_t *src_uv, int src_stride_uv, + uint8_t *dst_rgba, int dst_stride_rgba, + int width, int height) { + for (int y = 0; y < height; ++y) { + const uint8_t *y_row = src_y + y * src_stride_y; + const uint8_t *uv_row = src_uv + (y / 2) * src_stride_uv; + uint8_t *dst = dst_rgba + y * dst_stride_rgba; + for (int x = 0; x < width; ++x) { + const int uv_x = (x & ~1); + uint8_t r, g, b; + yuv_to_rgb(y_row[x], uv_row[uv_x], uv_row[uv_x + 1], &r, &g, &b); + dst[4 * x + 0] = r; + dst[4 * x + 1] = g; + dst[4 * x + 2] = b; + dst[4 * x + 3] = 255; + } + } +} + +} // namespace yuv diff --git a/openpilot/common/yuv.h b/openpilot/common/yuv.h new file mode 100644 index 0000000000..7ab096f764 --- /dev/null +++ b/openpilot/common/yuv.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +// NV12: Y plane + interleaved UV. I420: planar Y, U, V. + +namespace yuv { + +// Deinterleave NV12 UV into planar I420. +void nv12_to_i420(const uint8_t *src_y, int src_stride_y, + const uint8_t *src_uv, int src_stride_uv, + uint8_t *dst_y, int dst_stride_y, + uint8_t *dst_u, int dst_stride_u, + uint8_t *dst_v, int dst_stride_v, + int width, int height); + +// Interleave planar I420 UV into NV12. +void i420_to_nv12(const uint8_t *src_y, int src_stride_y, + const uint8_t *src_u, int src_stride_u, + const uint8_t *src_v, int src_stride_v, + uint8_t *dst_y, int dst_stride_y, + uint8_t *dst_uv, int dst_stride_uv, + int width, int height); + +// Point-sample scale I420 (equivalent to libyuv::I420Scale + kFilterNone). +void i420_scale(const uint8_t *src_y, int src_stride_y, + const uint8_t *src_u, int src_stride_u, + const uint8_t *src_v, int src_stride_v, + int src_width, int src_height, + uint8_t *dst_y, int dst_stride_y, + uint8_t *dst_u, int dst_stride_u, + uint8_t *dst_v, int dst_stride_v, + int dst_width, int dst_height); + +// Convert NV12 to packed RGBA (R,G,B,A bytes — suitable for GL_RGBA). +// BT.601 limited-range, matching common libyuv defaults. +void nv12_to_rgba(const uint8_t *src_y, int src_stride_y, + const uint8_t *src_uv, int src_stride_uv, + uint8_t *dst_rgba, int dst_stride_rgba, + int width, int height); + +} diff --git a/openpilot/selfdrive b/openpilot/selfdrive deleted file mode 120000 index e005fd0d04..0000000000 --- a/openpilot/selfdrive +++ /dev/null @@ -1 +0,0 @@ -../selfdrive/ \ No newline at end of file diff --git a/selfdrive/car/__init__.py b/openpilot/selfdrive/__init__.py similarity index 100% rename from selfdrive/car/__init__.py rename to openpilot/selfdrive/__init__.py diff --git a/selfdrive/assets/.gitignore b/openpilot/selfdrive/assets/.gitignore similarity index 100% rename from selfdrive/assets/.gitignore rename to openpilot/selfdrive/assets/.gitignore diff --git a/selfdrive/assets/assets.qrc b/openpilot/selfdrive/assets/assets.qrc similarity index 100% rename from selfdrive/assets/assets.qrc rename to openpilot/selfdrive/assets/assets.qrc diff --git a/selfdrive/assets/body/awake.gif b/openpilot/selfdrive/assets/body/awake.gif similarity index 100% rename from selfdrive/assets/body/awake.gif rename to openpilot/selfdrive/assets/body/awake.gif diff --git a/selfdrive/assets/body/sleep.gif b/openpilot/selfdrive/assets/body/sleep.gif similarity index 100% rename from selfdrive/assets/body/sleep.gif rename to openpilot/selfdrive/assets/body/sleep.gif diff --git a/selfdrive/assets/compress-images.sh b/openpilot/selfdrive/assets/compress-images.sh similarity index 100% rename from selfdrive/assets/compress-images.sh rename to openpilot/selfdrive/assets/compress-images.sh diff --git a/selfdrive/assets/fonts/Audiowide-Regular.ttf b/openpilot/selfdrive/assets/fonts/Audiowide-Regular.ttf similarity index 100% rename from selfdrive/assets/fonts/Audiowide-Regular.ttf rename to openpilot/selfdrive/assets/fonts/Audiowide-Regular.ttf diff --git a/selfdrive/assets/fonts/Inter-Black.ttf b/openpilot/selfdrive/assets/fonts/Inter-Black.ttf similarity index 100% rename from selfdrive/assets/fonts/Inter-Black.ttf rename to openpilot/selfdrive/assets/fonts/Inter-Black.ttf diff --git a/selfdrive/assets/fonts/Inter-Bold.ttf b/openpilot/selfdrive/assets/fonts/Inter-Bold.ttf similarity index 100% rename from selfdrive/assets/fonts/Inter-Bold.ttf rename to openpilot/selfdrive/assets/fonts/Inter-Bold.ttf diff --git a/selfdrive/assets/fonts/Inter-ExtraBold.ttf b/openpilot/selfdrive/assets/fonts/Inter-ExtraBold.ttf similarity index 100% rename from selfdrive/assets/fonts/Inter-ExtraBold.ttf rename to openpilot/selfdrive/assets/fonts/Inter-ExtraBold.ttf diff --git a/selfdrive/assets/fonts/Inter-ExtraLight.ttf b/openpilot/selfdrive/assets/fonts/Inter-ExtraLight.ttf similarity index 100% rename from selfdrive/assets/fonts/Inter-ExtraLight.ttf rename to openpilot/selfdrive/assets/fonts/Inter-ExtraLight.ttf diff --git a/selfdrive/assets/fonts/Inter-Light.ttf b/openpilot/selfdrive/assets/fonts/Inter-Light.ttf similarity index 100% rename from selfdrive/assets/fonts/Inter-Light.ttf rename to openpilot/selfdrive/assets/fonts/Inter-Light.ttf diff --git a/selfdrive/assets/fonts/Inter-Medium.ttf b/openpilot/selfdrive/assets/fonts/Inter-Medium.ttf similarity index 100% rename from selfdrive/assets/fonts/Inter-Medium.ttf rename to openpilot/selfdrive/assets/fonts/Inter-Medium.ttf diff --git a/selfdrive/assets/fonts/Inter-Regular.ttf b/openpilot/selfdrive/assets/fonts/Inter-Regular.ttf similarity index 100% rename from selfdrive/assets/fonts/Inter-Regular.ttf rename to openpilot/selfdrive/assets/fonts/Inter-Regular.ttf diff --git a/selfdrive/assets/fonts/Inter-SemiBold.ttf b/openpilot/selfdrive/assets/fonts/Inter-SemiBold.ttf similarity index 100% rename from selfdrive/assets/fonts/Inter-SemiBold.ttf rename to openpilot/selfdrive/assets/fonts/Inter-SemiBold.ttf diff --git a/selfdrive/assets/fonts/Inter-Thin.ttf b/openpilot/selfdrive/assets/fonts/Inter-Thin.ttf similarity index 100% rename from selfdrive/assets/fonts/Inter-Thin.ttf rename to openpilot/selfdrive/assets/fonts/Inter-Thin.ttf diff --git a/selfdrive/assets/fonts/JetBrainsMono-Medium.ttf b/openpilot/selfdrive/assets/fonts/JetBrainsMono-Medium.ttf similarity index 100% rename from selfdrive/assets/fonts/JetBrainsMono-Medium.ttf rename to openpilot/selfdrive/assets/fonts/JetBrainsMono-Medium.ttf diff --git a/selfdrive/assets/fonts/NotoColorEmoji.ttf b/openpilot/selfdrive/assets/fonts/NotoColorEmoji.ttf similarity index 100% rename from selfdrive/assets/fonts/NotoColorEmoji.ttf rename to openpilot/selfdrive/assets/fonts/NotoColorEmoji.ttf diff --git a/selfdrive/assets/fonts/process.py b/openpilot/selfdrive/assets/fonts/process.py similarity index 100% rename from selfdrive/assets/fonts/process.py rename to openpilot/selfdrive/assets/fonts/process.py diff --git a/selfdrive/assets/fonts/unifont.otf b/openpilot/selfdrive/assets/fonts/unifont.otf similarity index 100% rename from selfdrive/assets/fonts/unifont.otf rename to openpilot/selfdrive/assets/fonts/unifont.otf diff --git a/selfdrive/assets/icons/arrow-right.png b/openpilot/selfdrive/assets/icons/arrow-right.png similarity index 100% rename from selfdrive/assets/icons/arrow-right.png rename to openpilot/selfdrive/assets/icons/arrow-right.png diff --git a/selfdrive/assets/icons/backspace.png b/openpilot/selfdrive/assets/icons/backspace.png similarity index 100% rename from selfdrive/assets/icons/backspace.png rename to openpilot/selfdrive/assets/icons/backspace.png diff --git a/selfdrive/assets/icons/calibration.png b/openpilot/selfdrive/assets/icons/calibration.png similarity index 100% rename from selfdrive/assets/icons/calibration.png rename to openpilot/selfdrive/assets/icons/calibration.png diff --git a/selfdrive/assets/icons/capslock-fill.png b/openpilot/selfdrive/assets/icons/capslock-fill.png similarity index 100% rename from selfdrive/assets/icons/capslock-fill.png rename to openpilot/selfdrive/assets/icons/capslock-fill.png diff --git a/selfdrive/assets/icons/checkmark.png b/openpilot/selfdrive/assets/icons/checkmark.png similarity index 100% rename from selfdrive/assets/icons/checkmark.png rename to openpilot/selfdrive/assets/icons/checkmark.png diff --git a/selfdrive/assets/icons/checkmark.svg b/openpilot/selfdrive/assets/icons/checkmark.svg similarity index 100% rename from selfdrive/assets/icons/checkmark.svg rename to openpilot/selfdrive/assets/icons/checkmark.svg diff --git a/selfdrive/assets/icons/chevron_right.png b/openpilot/selfdrive/assets/icons/chevron_right.png similarity index 100% rename from selfdrive/assets/icons/chevron_right.png rename to openpilot/selfdrive/assets/icons/chevron_right.png diff --git a/selfdrive/assets/icons/chffr_wheel.png b/openpilot/selfdrive/assets/icons/chffr_wheel.png similarity index 100% rename from selfdrive/assets/icons/chffr_wheel.png rename to openpilot/selfdrive/assets/icons/chffr_wheel.png diff --git a/selfdrive/assets/icons/circled_check.png b/openpilot/selfdrive/assets/icons/circled_check.png similarity index 100% rename from selfdrive/assets/icons/circled_check.png rename to openpilot/selfdrive/assets/icons/circled_check.png diff --git a/selfdrive/assets/icons/circled_check.svg b/openpilot/selfdrive/assets/icons/circled_check.svg similarity index 100% rename from selfdrive/assets/icons/circled_check.svg rename to openpilot/selfdrive/assets/icons/circled_check.svg diff --git a/selfdrive/assets/icons/circled_slash.png b/openpilot/selfdrive/assets/icons/circled_slash.png similarity index 100% rename from selfdrive/assets/icons/circled_slash.png rename to openpilot/selfdrive/assets/icons/circled_slash.png diff --git a/selfdrive/assets/icons/circled_slash.svg b/openpilot/selfdrive/assets/icons/circled_slash.svg similarity index 100% rename from selfdrive/assets/icons/circled_slash.svg rename to openpilot/selfdrive/assets/icons/circled_slash.svg diff --git a/selfdrive/assets/icons/close.png b/openpilot/selfdrive/assets/icons/close.png similarity index 100% rename from selfdrive/assets/icons/close.png rename to openpilot/selfdrive/assets/icons/close.png diff --git a/selfdrive/assets/icons/close.svg b/openpilot/selfdrive/assets/icons/close.svg similarity index 100% rename from selfdrive/assets/icons/close.svg rename to openpilot/selfdrive/assets/icons/close.svg diff --git a/selfdrive/assets/icons/close2.png b/openpilot/selfdrive/assets/icons/close2.png similarity index 100% rename from selfdrive/assets/icons/close2.png rename to openpilot/selfdrive/assets/icons/close2.png diff --git a/selfdrive/assets/icons/close2.svg b/openpilot/selfdrive/assets/icons/close2.svg similarity index 100% rename from selfdrive/assets/icons/close2.svg rename to openpilot/selfdrive/assets/icons/close2.svg diff --git a/selfdrive/assets/icons/couch.png b/openpilot/selfdrive/assets/icons/couch.png similarity index 100% rename from selfdrive/assets/icons/couch.png rename to openpilot/selfdrive/assets/icons/couch.png diff --git a/selfdrive/assets/icons/couch.svg b/openpilot/selfdrive/assets/icons/couch.svg similarity index 100% rename from selfdrive/assets/icons/couch.svg rename to openpilot/selfdrive/assets/icons/couch.svg diff --git a/selfdrive/assets/icons/disengage_on_accelerator.png b/openpilot/selfdrive/assets/icons/disengage_on_accelerator.png similarity index 100% rename from selfdrive/assets/icons/disengage_on_accelerator.png rename to openpilot/selfdrive/assets/icons/disengage_on_accelerator.png diff --git a/selfdrive/assets/icons/disengage_on_accelerator.svg b/openpilot/selfdrive/assets/icons/disengage_on_accelerator.svg similarity index 100% rename from selfdrive/assets/icons/disengage_on_accelerator.svg rename to openpilot/selfdrive/assets/icons/disengage_on_accelerator.svg diff --git a/selfdrive/assets/icons/driver_face.png b/openpilot/selfdrive/assets/icons/driver_face.png similarity index 100% rename from selfdrive/assets/icons/driver_face.png rename to openpilot/selfdrive/assets/icons/driver_face.png diff --git a/selfdrive/assets/icons/experimental.png b/openpilot/selfdrive/assets/icons/experimental.png similarity index 100% rename from selfdrive/assets/icons/experimental.png rename to openpilot/selfdrive/assets/icons/experimental.png diff --git a/selfdrive/assets/icons/experimental.svg b/openpilot/selfdrive/assets/icons/experimental.svg similarity index 100% rename from selfdrive/assets/icons/experimental.svg rename to openpilot/selfdrive/assets/icons/experimental.svg diff --git a/selfdrive/assets/icons/experimental_grey.png b/openpilot/selfdrive/assets/icons/experimental_grey.png similarity index 100% rename from selfdrive/assets/icons/experimental_grey.png rename to openpilot/selfdrive/assets/icons/experimental_grey.png diff --git a/selfdrive/assets/icons/experimental_grey.svg b/openpilot/selfdrive/assets/icons/experimental_grey.svg similarity index 100% rename from selfdrive/assets/icons/experimental_grey.svg rename to openpilot/selfdrive/assets/icons/experimental_grey.svg diff --git a/selfdrive/assets/icons/experimental_white.png b/openpilot/selfdrive/assets/icons/experimental_white.png similarity index 100% rename from selfdrive/assets/icons/experimental_white.png rename to openpilot/selfdrive/assets/icons/experimental_white.png diff --git a/selfdrive/assets/icons/experimental_white.svg b/openpilot/selfdrive/assets/icons/experimental_white.svg similarity index 100% rename from selfdrive/assets/icons/experimental_white.svg rename to openpilot/selfdrive/assets/icons/experimental_white.svg diff --git a/selfdrive/assets/icons/eye_closed.png b/openpilot/selfdrive/assets/icons/eye_closed.png similarity index 100% rename from selfdrive/assets/icons/eye_closed.png rename to openpilot/selfdrive/assets/icons/eye_closed.png diff --git a/selfdrive/assets/icons/eye_closed.svg b/openpilot/selfdrive/assets/icons/eye_closed.svg similarity index 100% rename from selfdrive/assets/icons/eye_closed.svg rename to openpilot/selfdrive/assets/icons/eye_closed.svg diff --git a/selfdrive/assets/icons/eye_open.png b/openpilot/selfdrive/assets/icons/eye_open.png similarity index 100% rename from selfdrive/assets/icons/eye_open.png rename to openpilot/selfdrive/assets/icons/eye_open.png diff --git a/selfdrive/assets/icons/eye_open.svg b/openpilot/selfdrive/assets/icons/eye_open.svg similarity index 100% rename from selfdrive/assets/icons/eye_open.svg rename to openpilot/selfdrive/assets/icons/eye_open.svg diff --git a/selfdrive/assets/icons/eyes_crossed.png b/openpilot/selfdrive/assets/icons/eyes_crossed.png similarity index 100% rename from selfdrive/assets/icons/eyes_crossed.png rename to openpilot/selfdrive/assets/icons/eyes_crossed.png diff --git a/selfdrive/assets/icons/eyes_open.png b/openpilot/selfdrive/assets/icons/eyes_open.png similarity index 100% rename from selfdrive/assets/icons/eyes_open.png rename to openpilot/selfdrive/assets/icons/eyes_open.png diff --git a/selfdrive/assets/icons/link.png b/openpilot/selfdrive/assets/icons/link.png similarity index 100% rename from selfdrive/assets/icons/link.png rename to openpilot/selfdrive/assets/icons/link.png diff --git a/selfdrive/assets/icons/lock_closed.png b/openpilot/selfdrive/assets/icons/lock_closed.png similarity index 100% rename from selfdrive/assets/icons/lock_closed.png rename to openpilot/selfdrive/assets/icons/lock_closed.png diff --git a/selfdrive/assets/icons/lock_closed.svg b/openpilot/selfdrive/assets/icons/lock_closed.svg similarity index 100% rename from selfdrive/assets/icons/lock_closed.svg rename to openpilot/selfdrive/assets/icons/lock_closed.svg diff --git a/selfdrive/assets/icons/menu.png b/openpilot/selfdrive/assets/icons/menu.png similarity index 100% rename from selfdrive/assets/icons/menu.png rename to openpilot/selfdrive/assets/icons/menu.png diff --git a/selfdrive/assets/icons/metric.png b/openpilot/selfdrive/assets/icons/metric.png similarity index 100% rename from selfdrive/assets/icons/metric.png rename to openpilot/selfdrive/assets/icons/metric.png diff --git a/selfdrive/assets/icons/microphone.png b/openpilot/selfdrive/assets/icons/microphone.png similarity index 100% rename from selfdrive/assets/icons/microphone.png rename to openpilot/selfdrive/assets/icons/microphone.png diff --git a/selfdrive/assets/icons/minus.png b/openpilot/selfdrive/assets/icons/minus.png similarity index 100% rename from selfdrive/assets/icons/minus.png rename to openpilot/selfdrive/assets/icons/minus.png diff --git a/selfdrive/assets/icons/monitoring.png b/openpilot/selfdrive/assets/icons/monitoring.png similarity index 100% rename from selfdrive/assets/icons/monitoring.png rename to openpilot/selfdrive/assets/icons/monitoring.png diff --git a/selfdrive/assets/icons/network.png b/openpilot/selfdrive/assets/icons/network.png similarity index 100% rename from selfdrive/assets/icons/network.png rename to openpilot/selfdrive/assets/icons/network.png diff --git a/selfdrive/assets/icons/road.png b/openpilot/selfdrive/assets/icons/road.png similarity index 100% rename from selfdrive/assets/icons/road.png rename to openpilot/selfdrive/assets/icons/road.png diff --git a/selfdrive/assets/icons/settings.png b/openpilot/selfdrive/assets/icons/settings.png similarity index 100% rename from selfdrive/assets/icons/settings.png rename to openpilot/selfdrive/assets/icons/settings.png diff --git a/selfdrive/assets/icons/shell.png b/openpilot/selfdrive/assets/icons/shell.png similarity index 100% rename from selfdrive/assets/icons/shell.png rename to openpilot/selfdrive/assets/icons/shell.png diff --git a/selfdrive/assets/icons/shift-fill.png b/openpilot/selfdrive/assets/icons/shift-fill.png similarity index 100% rename from selfdrive/assets/icons/shift-fill.png rename to openpilot/selfdrive/assets/icons/shift-fill.png diff --git a/selfdrive/assets/icons/shift.png b/openpilot/selfdrive/assets/icons/shift.png similarity index 100% rename from selfdrive/assets/icons/shift.png rename to openpilot/selfdrive/assets/icons/shift.png diff --git a/selfdrive/assets/icons/speed_limit.png b/openpilot/selfdrive/assets/icons/speed_limit.png similarity index 100% rename from selfdrive/assets/icons/speed_limit.png rename to openpilot/selfdrive/assets/icons/speed_limit.png diff --git a/selfdrive/assets/icons/triangle.png b/openpilot/selfdrive/assets/icons/triangle.png similarity index 100% rename from selfdrive/assets/icons/triangle.png rename to openpilot/selfdrive/assets/icons/triangle.png diff --git a/selfdrive/assets/icons/triangle.svg b/openpilot/selfdrive/assets/icons/triangle.svg similarity index 100% rename from selfdrive/assets/icons/triangle.svg rename to openpilot/selfdrive/assets/icons/triangle.svg diff --git a/selfdrive/assets/icons/warning.png b/openpilot/selfdrive/assets/icons/warning.png similarity index 100% rename from selfdrive/assets/icons/warning.png rename to openpilot/selfdrive/assets/icons/warning.png diff --git a/selfdrive/assets/icons/wifi_strength_full.png b/openpilot/selfdrive/assets/icons/wifi_strength_full.png similarity index 100% rename from selfdrive/assets/icons/wifi_strength_full.png rename to openpilot/selfdrive/assets/icons/wifi_strength_full.png diff --git a/selfdrive/assets/icons/wifi_strength_full.svg b/openpilot/selfdrive/assets/icons/wifi_strength_full.svg similarity index 100% rename from selfdrive/assets/icons/wifi_strength_full.svg rename to openpilot/selfdrive/assets/icons/wifi_strength_full.svg diff --git a/selfdrive/assets/icons/wifi_strength_high.png b/openpilot/selfdrive/assets/icons/wifi_strength_high.png similarity index 100% rename from selfdrive/assets/icons/wifi_strength_high.png rename to openpilot/selfdrive/assets/icons/wifi_strength_high.png diff --git a/selfdrive/assets/icons/wifi_strength_high.svg b/openpilot/selfdrive/assets/icons/wifi_strength_high.svg similarity index 100% rename from selfdrive/assets/icons/wifi_strength_high.svg rename to openpilot/selfdrive/assets/icons/wifi_strength_high.svg diff --git a/selfdrive/assets/icons/wifi_strength_low.png b/openpilot/selfdrive/assets/icons/wifi_strength_low.png similarity index 100% rename from selfdrive/assets/icons/wifi_strength_low.png rename to openpilot/selfdrive/assets/icons/wifi_strength_low.png diff --git a/selfdrive/assets/icons/wifi_strength_low.svg b/openpilot/selfdrive/assets/icons/wifi_strength_low.svg similarity index 100% rename from selfdrive/assets/icons/wifi_strength_low.svg rename to openpilot/selfdrive/assets/icons/wifi_strength_low.svg diff --git a/selfdrive/assets/icons/wifi_strength_medium.png b/openpilot/selfdrive/assets/icons/wifi_strength_medium.png similarity index 100% rename from selfdrive/assets/icons/wifi_strength_medium.png rename to openpilot/selfdrive/assets/icons/wifi_strength_medium.png diff --git a/selfdrive/assets/icons/wifi_strength_medium.svg b/openpilot/selfdrive/assets/icons/wifi_strength_medium.svg similarity index 100% rename from selfdrive/assets/icons/wifi_strength_medium.svg rename to openpilot/selfdrive/assets/icons/wifi_strength_medium.svg diff --git a/selfdrive/assets/icons/wifi_uploading.png b/openpilot/selfdrive/assets/icons/wifi_uploading.png similarity index 100% rename from selfdrive/assets/icons/wifi_uploading.png rename to openpilot/selfdrive/assets/icons/wifi_uploading.png diff --git a/selfdrive/assets/icons/wifi_uploading.svg b/openpilot/selfdrive/assets/icons/wifi_uploading.svg similarity index 100% rename from selfdrive/assets/icons/wifi_uploading.svg rename to openpilot/selfdrive/assets/icons/wifi_uploading.svg diff --git a/selfdrive/assets/icons_mici/adb_short.png b/openpilot/selfdrive/assets/icons_mici/adb_short.png similarity index 100% rename from selfdrive/assets/icons_mici/adb_short.png rename to openpilot/selfdrive/assets/icons_mici/adb_short.png diff --git a/selfdrive/assets/icons_mici/alerts_bell.png b/openpilot/selfdrive/assets/icons_mici/alerts_bell.png similarity index 100% rename from selfdrive/assets/icons_mici/alerts_bell.png rename to openpilot/selfdrive/assets/icons_mici/alerts_bell.png diff --git a/selfdrive/assets/icons_mici/alerts_pill.png b/openpilot/selfdrive/assets/icons_mici/alerts_pill.png similarity index 100% rename from selfdrive/assets/icons_mici/alerts_pill.png rename to openpilot/selfdrive/assets/icons_mici/alerts_pill.png diff --git a/selfdrive/assets/icons_mici/body.png b/openpilot/selfdrive/assets/icons_mici/body.png similarity index 100% rename from selfdrive/assets/icons_mici/body.png rename to openpilot/selfdrive/assets/icons_mici/body.png diff --git a/selfdrive/assets/icons_mici/buttons/button_circle.png b/openpilot/selfdrive/assets/icons_mici/buttons/button_circle.png similarity index 100% rename from selfdrive/assets/icons_mici/buttons/button_circle.png rename to openpilot/selfdrive/assets/icons_mici/buttons/button_circle.png diff --git a/selfdrive/assets/icons_mici/buttons/button_circle_disabled.png b/openpilot/selfdrive/assets/icons_mici/buttons/button_circle_disabled.png similarity index 100% rename from selfdrive/assets/icons_mici/buttons/button_circle_disabled.png rename to openpilot/selfdrive/assets/icons_mici/buttons/button_circle_disabled.png diff --git a/selfdrive/assets/icons_mici/buttons/button_circle_pressed.png b/openpilot/selfdrive/assets/icons_mici/buttons/button_circle_pressed.png similarity index 100% rename from selfdrive/assets/icons_mici/buttons/button_circle_pressed.png rename to openpilot/selfdrive/assets/icons_mici/buttons/button_circle_pressed.png diff --git a/selfdrive/assets/icons_mici/buttons/button_circle_red.png b/openpilot/selfdrive/assets/icons_mici/buttons/button_circle_red.png similarity index 100% rename from selfdrive/assets/icons_mici/buttons/button_circle_red.png rename to openpilot/selfdrive/assets/icons_mici/buttons/button_circle_red.png diff --git a/selfdrive/assets/icons_mici/buttons/button_circle_red_pressed.png b/openpilot/selfdrive/assets/icons_mici/buttons/button_circle_red_pressed.png similarity index 100% rename from selfdrive/assets/icons_mici/buttons/button_circle_red_pressed.png rename to openpilot/selfdrive/assets/icons_mici/buttons/button_circle_red_pressed.png diff --git a/selfdrive/assets/icons_mici/buttons/button_rectangle.png b/openpilot/selfdrive/assets/icons_mici/buttons/button_rectangle.png similarity index 100% rename from selfdrive/assets/icons_mici/buttons/button_rectangle.png rename to openpilot/selfdrive/assets/icons_mici/buttons/button_rectangle.png diff --git a/selfdrive/assets/icons_mici/buttons/button_rectangle_disabled.png b/openpilot/selfdrive/assets/icons_mici/buttons/button_rectangle_disabled.png similarity index 100% rename from selfdrive/assets/icons_mici/buttons/button_rectangle_disabled.png rename to openpilot/selfdrive/assets/icons_mici/buttons/button_rectangle_disabled.png diff --git a/selfdrive/assets/icons_mici/buttons/button_rectangle_pressed.png b/openpilot/selfdrive/assets/icons_mici/buttons/button_rectangle_pressed.png similarity index 100% rename from selfdrive/assets/icons_mici/buttons/button_rectangle_pressed.png rename to openpilot/selfdrive/assets/icons_mici/buttons/button_rectangle_pressed.png diff --git a/selfdrive/assets/icons_mici/buttons/slider_bg.png b/openpilot/selfdrive/assets/icons_mici/buttons/slider_bg.png similarity index 100% rename from selfdrive/assets/icons_mici/buttons/slider_bg.png rename to openpilot/selfdrive/assets/icons_mici/buttons/slider_bg.png diff --git a/selfdrive/assets/icons_mici/buttons/toggle_dot_disabled.png b/openpilot/selfdrive/assets/icons_mici/buttons/toggle_dot_disabled.png similarity index 100% rename from selfdrive/assets/icons_mici/buttons/toggle_dot_disabled.png rename to openpilot/selfdrive/assets/icons_mici/buttons/toggle_dot_disabled.png diff --git a/selfdrive/assets/icons_mici/buttons/toggle_dot_enabled.png b/openpilot/selfdrive/assets/icons_mici/buttons/toggle_dot_enabled.png similarity index 100% rename from selfdrive/assets/icons_mici/buttons/toggle_dot_enabled.png rename to openpilot/selfdrive/assets/icons_mici/buttons/toggle_dot_enabled.png diff --git a/selfdrive/assets/icons_mici/buttons/toggle_pill_disabled.png b/openpilot/selfdrive/assets/icons_mici/buttons/toggle_pill_disabled.png similarity index 100% rename from selfdrive/assets/icons_mici/buttons/toggle_pill_disabled.png rename to openpilot/selfdrive/assets/icons_mici/buttons/toggle_pill_disabled.png diff --git a/selfdrive/assets/icons_mici/buttons/toggle_pill_enabled.png b/openpilot/selfdrive/assets/icons_mici/buttons/toggle_pill_enabled.png similarity index 100% rename from selfdrive/assets/icons_mici/buttons/toggle_pill_enabled.png rename to openpilot/selfdrive/assets/icons_mici/buttons/toggle_pill_enabled.png diff --git a/selfdrive/assets/icons_mici/egpu.png b/openpilot/selfdrive/assets/icons_mici/egpu.png similarity index 100% rename from selfdrive/assets/icons_mici/egpu.png rename to openpilot/selfdrive/assets/icons_mici/egpu.png diff --git a/selfdrive/assets/icons_mici/egpu_gray.png b/openpilot/selfdrive/assets/icons_mici/egpu_gray.png similarity index 100% rename from selfdrive/assets/icons_mici/egpu_gray.png rename to openpilot/selfdrive/assets/icons_mici/egpu_gray.png diff --git a/selfdrive/assets/icons_mici/exclamation_point.png b/openpilot/selfdrive/assets/icons_mici/exclamation_point.png similarity index 100% rename from selfdrive/assets/icons_mici/exclamation_point.png rename to openpilot/selfdrive/assets/icons_mici/exclamation_point.png diff --git a/selfdrive/assets/icons_mici/experimental_mode.png b/openpilot/selfdrive/assets/icons_mici/experimental_mode.png similarity index 100% rename from selfdrive/assets/icons_mici/experimental_mode.png rename to openpilot/selfdrive/assets/icons_mici/experimental_mode.png diff --git a/selfdrive/assets/icons_mici/microphone.png b/openpilot/selfdrive/assets/icons_mici/microphone.png similarity index 100% rename from selfdrive/assets/icons_mici/microphone.png rename to openpilot/selfdrive/assets/icons_mici/microphone.png diff --git a/selfdrive/assets/icons_mici/offroad_alerts/big_alert.png b/openpilot/selfdrive/assets/icons_mici/offroad_alerts/big_alert.png similarity index 100% rename from selfdrive/assets/icons_mici/offroad_alerts/big_alert.png rename to openpilot/selfdrive/assets/icons_mici/offroad_alerts/big_alert.png diff --git a/selfdrive/assets/icons_mici/offroad_alerts/big_alert_pressed.png b/openpilot/selfdrive/assets/icons_mici/offroad_alerts/big_alert_pressed.png similarity index 100% rename from selfdrive/assets/icons_mici/offroad_alerts/big_alert_pressed.png rename to openpilot/selfdrive/assets/icons_mici/offroad_alerts/big_alert_pressed.png diff --git a/selfdrive/assets/icons_mici/offroad_alerts/green_wheel.png b/openpilot/selfdrive/assets/icons_mici/offroad_alerts/green_wheel.png similarity index 100% rename from selfdrive/assets/icons_mici/offroad_alerts/green_wheel.png rename to openpilot/selfdrive/assets/icons_mici/offroad_alerts/green_wheel.png diff --git a/selfdrive/assets/icons_mici/offroad_alerts/medium_alert.png b/openpilot/selfdrive/assets/icons_mici/offroad_alerts/medium_alert.png similarity index 100% rename from selfdrive/assets/icons_mici/offroad_alerts/medium_alert.png rename to openpilot/selfdrive/assets/icons_mici/offroad_alerts/medium_alert.png diff --git a/selfdrive/assets/icons_mici/offroad_alerts/medium_alert_pressed.png b/openpilot/selfdrive/assets/icons_mici/offroad_alerts/medium_alert_pressed.png similarity index 100% rename from selfdrive/assets/icons_mici/offroad_alerts/medium_alert_pressed.png rename to openpilot/selfdrive/assets/icons_mici/offroad_alerts/medium_alert_pressed.png diff --git a/selfdrive/assets/icons_mici/offroad_alerts/orange_warning.png b/openpilot/selfdrive/assets/icons_mici/offroad_alerts/orange_warning.png similarity index 100% rename from selfdrive/assets/icons_mici/offroad_alerts/orange_warning.png rename to openpilot/selfdrive/assets/icons_mici/offroad_alerts/orange_warning.png diff --git a/selfdrive/assets/icons_mici/offroad_alerts/red_warning.png b/openpilot/selfdrive/assets/icons_mici/offroad_alerts/red_warning.png similarity index 100% rename from selfdrive/assets/icons_mici/offroad_alerts/red_warning.png rename to openpilot/selfdrive/assets/icons_mici/offroad_alerts/red_warning.png diff --git a/selfdrive/assets/icons_mici/offroad_alerts/small_alert.png b/openpilot/selfdrive/assets/icons_mici/offroad_alerts/small_alert.png similarity index 100% rename from selfdrive/assets/icons_mici/offroad_alerts/small_alert.png rename to openpilot/selfdrive/assets/icons_mici/offroad_alerts/small_alert.png diff --git a/selfdrive/assets/icons_mici/offroad_alerts/small_alert_pressed.png b/openpilot/selfdrive/assets/icons_mici/offroad_alerts/small_alert_pressed.png similarity index 100% rename from selfdrive/assets/icons_mici/offroad_alerts/small_alert_pressed.png rename to openpilot/selfdrive/assets/icons_mici/offroad_alerts/small_alert_pressed.png diff --git a/selfdrive/assets/icons_mici/onroad/blind_spot_left.png b/openpilot/selfdrive/assets/icons_mici/onroad/blind_spot_left.png similarity index 100% rename from selfdrive/assets/icons_mici/onroad/blind_spot_left.png rename to openpilot/selfdrive/assets/icons_mici/onroad/blind_spot_left.png diff --git a/selfdrive/assets/icons_mici/onroad/bookmark.png b/openpilot/selfdrive/assets/icons_mici/onroad/bookmark.png similarity index 100% rename from selfdrive/assets/icons_mici/onroad/bookmark.png rename to openpilot/selfdrive/assets/icons_mici/onroad/bookmark.png diff --git a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png b/openpilot/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png similarity index 100% rename from selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png rename to openpilot/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png diff --git a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_cone.png b/openpilot/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_cone.png similarity index 100% rename from selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_cone.png rename to openpilot/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_cone.png diff --git a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png b/openpilot/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png similarity index 100% rename from selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png rename to openpilot/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png diff --git a/selfdrive/assets/icons_mici/onroad/eye_fill.png b/openpilot/selfdrive/assets/icons_mici/onroad/eye_fill.png similarity index 100% rename from selfdrive/assets/icons_mici/onroad/eye_fill.png rename to openpilot/selfdrive/assets/icons_mici/onroad/eye_fill.png diff --git a/selfdrive/assets/icons_mici/onroad/eye_orange.png b/openpilot/selfdrive/assets/icons_mici/onroad/eye_orange.png similarity index 100% rename from selfdrive/assets/icons_mici/onroad/eye_orange.png rename to openpilot/selfdrive/assets/icons_mici/onroad/eye_orange.png diff --git a/selfdrive/assets/icons_mici/onroad/glasses.png b/openpilot/selfdrive/assets/icons_mici/onroad/glasses.png similarity index 100% rename from selfdrive/assets/icons_mici/onroad/glasses.png rename to openpilot/selfdrive/assets/icons_mici/onroad/glasses.png diff --git a/selfdrive/assets/icons_mici/onroad/onroad_fade.png b/openpilot/selfdrive/assets/icons_mici/onroad/onroad_fade.png similarity index 100% rename from selfdrive/assets/icons_mici/onroad/onroad_fade.png rename to openpilot/selfdrive/assets/icons_mici/onroad/onroad_fade.png diff --git a/selfdrive/assets/icons_mici/onroad/turn_signal_left.png b/openpilot/selfdrive/assets/icons_mici/onroad/turn_signal_left.png similarity index 100% rename from selfdrive/assets/icons_mici/onroad/turn_signal_left.png rename to openpilot/selfdrive/assets/icons_mici/onroad/turn_signal_left.png diff --git a/selfdrive/assets/icons_mici/settings.png b/openpilot/selfdrive/assets/icons_mici/settings.png similarity index 100% rename from selfdrive/assets/icons_mici/settings.png rename to openpilot/selfdrive/assets/icons_mici/settings.png diff --git a/selfdrive/assets/icons_mici/settings/comma_icon.png b/openpilot/selfdrive/assets/icons_mici/settings/comma_icon.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/comma_icon.png rename to openpilot/selfdrive/assets/icons_mici/settings/comma_icon.png diff --git a/selfdrive/assets/icons_mici/settings/developer/ssh.png b/openpilot/selfdrive/assets/icons_mici/settings/developer/ssh.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/developer/ssh.png rename to openpilot/selfdrive/assets/icons_mici/settings/developer/ssh.png diff --git a/selfdrive/assets/icons_mici/settings/developer_icon.png b/openpilot/selfdrive/assets/icons_mici/settings/developer_icon.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/developer_icon.png rename to openpilot/selfdrive/assets/icons_mici/settings/developer_icon.png diff --git a/selfdrive/assets/icons_mici/settings/device/cameras.png b/openpilot/selfdrive/assets/icons_mici/settings/device/cameras.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/device/cameras.png rename to openpilot/selfdrive/assets/icons_mici/settings/device/cameras.png diff --git a/selfdrive/assets/icons_mici/settings/device/fcc_logo.png b/openpilot/selfdrive/assets/icons_mici/settings/device/fcc_logo.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/device/fcc_logo.png rename to openpilot/selfdrive/assets/icons_mici/settings/device/fcc_logo.png diff --git a/selfdrive/assets/icons_mici/settings/device/info.png b/openpilot/selfdrive/assets/icons_mici/settings/device/info.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/device/info.png rename to openpilot/selfdrive/assets/icons_mici/settings/device/info.png diff --git a/selfdrive/assets/icons_mici/settings/device/lkas.png b/openpilot/selfdrive/assets/icons_mici/settings/device/lkas.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/device/lkas.png rename to openpilot/selfdrive/assets/icons_mici/settings/device/lkas.png diff --git a/selfdrive/assets/icons_mici/settings/device/pair.png b/openpilot/selfdrive/assets/icons_mici/settings/device/pair.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/device/pair.png rename to openpilot/selfdrive/assets/icons_mici/settings/device/pair.png diff --git a/selfdrive/assets/icons_mici/settings/device/power.png b/openpilot/selfdrive/assets/icons_mici/settings/device/power.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/device/power.png rename to openpilot/selfdrive/assets/icons_mici/settings/device/power.png diff --git a/selfdrive/assets/icons_mici/settings/device/reboot.png b/openpilot/selfdrive/assets/icons_mici/settings/device/reboot.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/device/reboot.png rename to openpilot/selfdrive/assets/icons_mici/settings/device/reboot.png diff --git a/selfdrive/assets/icons_mici/settings/device/uninstall.png b/openpilot/selfdrive/assets/icons_mici/settings/device/uninstall.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/device/uninstall.png rename to openpilot/selfdrive/assets/icons_mici/settings/device/uninstall.png diff --git a/selfdrive/assets/icons_mici/settings/device/up_to_date.png b/openpilot/selfdrive/assets/icons_mici/settings/device/up_to_date.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/device/up_to_date.png rename to openpilot/selfdrive/assets/icons_mici/settings/device/up_to_date.png diff --git a/selfdrive/assets/icons_mici/settings/device/update.png b/openpilot/selfdrive/assets/icons_mici/settings/device/update.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/device/update.png rename to openpilot/selfdrive/assets/icons_mici/settings/device/update.png diff --git a/selfdrive/assets/icons_mici/settings/device_icon.png b/openpilot/selfdrive/assets/icons_mici/settings/device_icon.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/device_icon.png rename to openpilot/selfdrive/assets/icons_mici/settings/device_icon.png diff --git a/selfdrive/assets/icons_mici/settings/firehose.png b/openpilot/selfdrive/assets/icons_mici/settings/firehose.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/firehose.png rename to openpilot/selfdrive/assets/icons_mici/settings/firehose.png diff --git a/selfdrive/assets/icons_mici/settings/horizontal_scroll_indicator.png b/openpilot/selfdrive/assets/icons_mici/settings/horizontal_scroll_indicator.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/horizontal_scroll_indicator.png rename to openpilot/selfdrive/assets/icons_mici/settings/horizontal_scroll_indicator.png diff --git a/selfdrive/assets/icons_mici/settings/keyboard/backspace.png b/openpilot/selfdrive/assets/icons_mici/settings/keyboard/backspace.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/keyboard/backspace.png rename to openpilot/selfdrive/assets/icons_mici/settings/keyboard/backspace.png diff --git a/selfdrive/assets/icons_mici/settings/keyboard/caps_lock.png b/openpilot/selfdrive/assets/icons_mici/settings/keyboard/caps_lock.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/keyboard/caps_lock.png rename to openpilot/selfdrive/assets/icons_mici/settings/keyboard/caps_lock.png diff --git a/selfdrive/assets/icons_mici/settings/keyboard/caps_lower.png b/openpilot/selfdrive/assets/icons_mici/settings/keyboard/caps_lower.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/keyboard/caps_lower.png rename to openpilot/selfdrive/assets/icons_mici/settings/keyboard/caps_lower.png diff --git a/selfdrive/assets/icons_mici/settings/keyboard/caps_upper.png b/openpilot/selfdrive/assets/icons_mici/settings/keyboard/caps_upper.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/keyboard/caps_upper.png rename to openpilot/selfdrive/assets/icons_mici/settings/keyboard/caps_upper.png diff --git a/selfdrive/assets/icons_mici/settings/keyboard/enter.png b/openpilot/selfdrive/assets/icons_mici/settings/keyboard/enter.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/keyboard/enter.png rename to openpilot/selfdrive/assets/icons_mici/settings/keyboard/enter.png diff --git a/selfdrive/assets/icons_mici/settings/keyboard/enter_disabled.png b/openpilot/selfdrive/assets/icons_mici/settings/keyboard/enter_disabled.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/keyboard/enter_disabled.png rename to openpilot/selfdrive/assets/icons_mici/settings/keyboard/enter_disabled.png diff --git a/selfdrive/assets/icons_mici/settings/keyboard/keyboard_background.png b/openpilot/selfdrive/assets/icons_mici/settings/keyboard/keyboard_background.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/keyboard/keyboard_background.png rename to openpilot/selfdrive/assets/icons_mici/settings/keyboard/keyboard_background.png diff --git a/selfdrive/assets/icons_mici/settings/keyboard/space.png b/openpilot/selfdrive/assets/icons_mici/settings/keyboard/space.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/keyboard/space.png rename to openpilot/selfdrive/assets/icons_mici/settings/keyboard/space.png diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_full.png b/openpilot/selfdrive/assets/icons_mici/settings/network/cell_strength_full.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/cell_strength_full.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/cell_strength_full.png diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_high.png b/openpilot/selfdrive/assets/icons_mici/settings/network/cell_strength_high.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/cell_strength_high.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/cell_strength_high.png diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_low.png b/openpilot/selfdrive/assets/icons_mici/settings/network/cell_strength_low.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/cell_strength_low.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/cell_strength_low.png diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_medium.png b/openpilot/selfdrive/assets/icons_mici/settings/network/cell_strength_medium.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/cell_strength_medium.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/cell_strength_medium.png diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_none.png b/openpilot/selfdrive/assets/icons_mici/settings/network/cell_strength_none.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/cell_strength_none.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/cell_strength_none.png diff --git a/selfdrive/assets/icons_mici/settings/network/new/forget_button.png b/openpilot/selfdrive/assets/icons_mici/settings/network/new/forget_button.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/new/forget_button.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/new/forget_button.png diff --git a/selfdrive/assets/icons_mici/settings/network/new/forget_button_pressed.png b/openpilot/selfdrive/assets/icons_mici/settings/network/new/forget_button_pressed.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/new/forget_button_pressed.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/new/forget_button_pressed.png diff --git a/selfdrive/assets/icons_mici/settings/network/new/lock.png b/openpilot/selfdrive/assets/icons_mici/settings/network/new/lock.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/new/lock.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/new/lock.png diff --git a/selfdrive/assets/icons_mici/settings/network/new/trash.png b/openpilot/selfdrive/assets/icons_mici/settings/network/new/trash.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/new/trash.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/new/trash.png diff --git a/selfdrive/assets/icons_mici/settings/network/tethering.png b/openpilot/selfdrive/assets/icons_mici/settings/network/tethering.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/tethering.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/tethering.png diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_full.png b/openpilot/selfdrive/assets/icons_mici/settings/network/wifi_strength_full.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/wifi_strength_full.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/wifi_strength_full.png diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_low.png b/openpilot/selfdrive/assets/icons_mici/settings/network/wifi_strength_low.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/wifi_strength_low.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/wifi_strength_low.png diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_medium.png b/openpilot/selfdrive/assets/icons_mici/settings/network/wifi_strength_medium.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/wifi_strength_medium.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/wifi_strength_medium.png diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_none.png b/openpilot/selfdrive/assets/icons_mici/settings/network/wifi_strength_none.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/wifi_strength_none.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/wifi_strength_none.png diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_slash.png b/openpilot/selfdrive/assets/icons_mici/settings/network/wifi_strength_slash.png similarity index 100% rename from selfdrive/assets/icons_mici/settings/network/wifi_strength_slash.png rename to openpilot/selfdrive/assets/icons_mici/settings/network/wifi_strength_slash.png diff --git a/openpilot/selfdrive/assets/icons_mici/settings/software.png b/openpilot/selfdrive/assets/icons_mici/settings/software.png new file mode 100644 index 0000000000..5cf528cbdd --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/settings/software.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4c38772e6080aa4b8bf5212d3619e949775468c64f3edb88a1a426d767c38d2 +size 1579 diff --git a/selfdrive/assets/icons_mici/setup/cancel.png b/openpilot/selfdrive/assets/icons_mici/setup/cancel.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/cancel.png rename to openpilot/selfdrive/assets/icons_mici/setup/cancel.png diff --git a/selfdrive/assets/icons_mici/setup/continue.png b/openpilot/selfdrive/assets/icons_mici/setup/continue.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/continue.png rename to openpilot/selfdrive/assets/icons_mici/setup/continue.png diff --git a/selfdrive/assets/icons_mici/setup/continue_disabled.png b/openpilot/selfdrive/assets/icons_mici/setup/continue_disabled.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/continue_disabled.png rename to openpilot/selfdrive/assets/icons_mici/setup/continue_disabled.png diff --git a/selfdrive/assets/icons_mici/setup/continue_pressed.png b/openpilot/selfdrive/assets/icons_mici/setup/continue_pressed.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/continue_pressed.png rename to openpilot/selfdrive/assets/icons_mici/setup/continue_pressed.png diff --git a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png b/openpilot/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png rename to openpilot/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png diff --git a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png b/openpilot/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png rename to openpilot/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png diff --git a/selfdrive/assets/icons_mici/setup/factory_reset.png b/openpilot/selfdrive/assets/icons_mici/setup/factory_reset.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/factory_reset.png rename to openpilot/selfdrive/assets/icons_mici/setup/factory_reset.png diff --git a/selfdrive/assets/icons_mici/setup/green_dm.png b/openpilot/selfdrive/assets/icons_mici/setup/green_dm.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/green_dm.png rename to openpilot/selfdrive/assets/icons_mici/setup/green_dm.png diff --git a/selfdrive/assets/icons_mici/setup/green_info.png b/openpilot/selfdrive/assets/icons_mici/setup/green_info.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/green_info.png rename to openpilot/selfdrive/assets/icons_mici/setup/green_info.png diff --git a/selfdrive/assets/icons_mici/setup/orange_dm.png b/openpilot/selfdrive/assets/icons_mici/setup/orange_dm.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/orange_dm.png rename to openpilot/selfdrive/assets/icons_mici/setup/orange_dm.png diff --git a/selfdrive/assets/icons_mici/setup/red_warning.png b/openpilot/selfdrive/assets/icons_mici/setup/red_warning.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/red_warning.png rename to openpilot/selfdrive/assets/icons_mici/setup/red_warning.png diff --git a/selfdrive/assets/icons_mici/setup/reset_failed.png b/openpilot/selfdrive/assets/icons_mici/setup/reset_failed.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/reset_failed.png rename to openpilot/selfdrive/assets/icons_mici/setup/reset_failed.png diff --git a/selfdrive/assets/icons_mici/setup/restore.png b/openpilot/selfdrive/assets/icons_mici/setup/restore.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/restore.png rename to openpilot/selfdrive/assets/icons_mici/setup/restore.png diff --git a/selfdrive/assets/icons_mici/setup/small_button.png b/openpilot/selfdrive/assets/icons_mici/setup/small_button.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/small_button.png rename to openpilot/selfdrive/assets/icons_mici/setup/small_button.png diff --git a/selfdrive/assets/icons_mici/setup/small_button_disabled.png b/openpilot/selfdrive/assets/icons_mici/setup/small_button_disabled.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/small_button_disabled.png rename to openpilot/selfdrive/assets/icons_mici/setup/small_button_disabled.png diff --git a/selfdrive/assets/icons_mici/setup/small_button_pressed.png b/openpilot/selfdrive/assets/icons_mici/setup/small_button_pressed.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/small_button_pressed.png rename to openpilot/selfdrive/assets/icons_mici/setup/small_button_pressed.png diff --git a/selfdrive/assets/icons_mici/setup/small_slider/slider_arrow.png b/openpilot/selfdrive/assets/icons_mici/setup/small_slider/slider_arrow.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/small_slider/slider_arrow.png rename to openpilot/selfdrive/assets/icons_mici/setup/small_slider/slider_arrow.png diff --git a/selfdrive/assets/icons_mici/setup/small_slider/slider_bg_larger.png b/openpilot/selfdrive/assets/icons_mici/setup/small_slider/slider_bg_larger.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/small_slider/slider_bg_larger.png rename to openpilot/selfdrive/assets/icons_mici/setup/small_slider/slider_bg_larger.png diff --git a/selfdrive/assets/icons_mici/setup/small_slider/slider_black_rounded_rectangle.png b/openpilot/selfdrive/assets/icons_mici/setup/small_slider/slider_black_rounded_rectangle.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/small_slider/slider_black_rounded_rectangle.png rename to openpilot/selfdrive/assets/icons_mici/setup/small_slider/slider_black_rounded_rectangle.png diff --git a/selfdrive/assets/icons_mici/setup/small_slider/slider_black_rounded_rectangle_pressed.png b/openpilot/selfdrive/assets/icons_mici/setup/small_slider/slider_black_rounded_rectangle_pressed.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/small_slider/slider_black_rounded_rectangle_pressed.png rename to openpilot/selfdrive/assets/icons_mici/setup/small_slider/slider_black_rounded_rectangle_pressed.png diff --git a/selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle.png b/openpilot/selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle.png rename to openpilot/selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle.png diff --git a/selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle_pressed.png b/openpilot/selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle_pressed.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle_pressed.png rename to openpilot/selfdrive/assets/icons_mici/setup/small_slider/slider_green_rounded_rectangle_pressed.png diff --git a/selfdrive/assets/icons_mici/setup/start_button.png b/openpilot/selfdrive/assets/icons_mici/setup/start_button.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/start_button.png rename to openpilot/selfdrive/assets/icons_mici/setup/start_button.png diff --git a/selfdrive/assets/icons_mici/setup/start_button_pressed.png b/openpilot/selfdrive/assets/icons_mici/setup/start_button_pressed.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/start_button_pressed.png rename to openpilot/selfdrive/assets/icons_mici/setup/start_button_pressed.png diff --git a/selfdrive/assets/icons_mici/setup/warning.png b/openpilot/selfdrive/assets/icons_mici/setup/warning.png similarity index 100% rename from selfdrive/assets/icons_mici/setup/warning.png rename to openpilot/selfdrive/assets/icons_mici/setup/warning.png diff --git a/selfdrive/assets/icons_mici/ssh_short.png b/openpilot/selfdrive/assets/icons_mici/ssh_short.png similarity index 100% rename from selfdrive/assets/icons_mici/ssh_short.png rename to openpilot/selfdrive/assets/icons_mici/ssh_short.png diff --git a/selfdrive/assets/icons_mici/turn_intent_left.png b/openpilot/selfdrive/assets/icons_mici/turn_intent_left.png similarity index 100% rename from selfdrive/assets/icons_mici/turn_intent_left.png rename to openpilot/selfdrive/assets/icons_mici/turn_intent_left.png diff --git a/selfdrive/assets/icons_mici/wheel.png b/openpilot/selfdrive/assets/icons_mici/wheel.png similarity index 100% rename from selfdrive/assets/icons_mici/wheel.png rename to openpilot/selfdrive/assets/icons_mici/wheel.png diff --git a/selfdrive/assets/icons_mici/wheel_critical.png b/openpilot/selfdrive/assets/icons_mici/wheel_critical.png similarity index 100% rename from selfdrive/assets/icons_mici/wheel_critical.png rename to openpilot/selfdrive/assets/icons_mici/wheel_critical.png diff --git a/selfdrive/assets/images/button_continue_triangle.png b/openpilot/selfdrive/assets/images/button_continue_triangle.png similarity index 100% rename from selfdrive/assets/images/button_continue_triangle.png rename to openpilot/selfdrive/assets/images/button_continue_triangle.png diff --git a/selfdrive/assets/images/button_continue_triangle.svg b/openpilot/selfdrive/assets/images/button_continue_triangle.svg similarity index 100% rename from selfdrive/assets/images/button_continue_triangle.svg rename to openpilot/selfdrive/assets/images/button_continue_triangle.svg diff --git a/selfdrive/assets/images/button_flag.png b/openpilot/selfdrive/assets/images/button_flag.png similarity index 100% rename from selfdrive/assets/images/button_flag.png rename to openpilot/selfdrive/assets/images/button_flag.png diff --git a/selfdrive/assets/images/button_home.png b/openpilot/selfdrive/assets/images/button_home.png similarity index 100% rename from selfdrive/assets/images/button_home.png rename to openpilot/selfdrive/assets/images/button_home.png diff --git a/selfdrive/assets/images/button_settings.png b/openpilot/selfdrive/assets/images/button_settings.png similarity index 100% rename from selfdrive/assets/images/button_settings.png rename to openpilot/selfdrive/assets/images/button_settings.png diff --git a/selfdrive/assets/images/spinner_comma.png b/openpilot/selfdrive/assets/images/spinner_comma.png similarity index 100% rename from selfdrive/assets/images/spinner_comma.png rename to openpilot/selfdrive/assets/images/spinner_comma.png diff --git a/selfdrive/assets/images/spinner_track.png b/openpilot/selfdrive/assets/images/spinner_track.png similarity index 100% rename from selfdrive/assets/images/spinner_track.png rename to openpilot/selfdrive/assets/images/spinner_track.png diff --git a/selfdrive/assets/offroad/fcc.html b/openpilot/selfdrive/assets/offroad/fcc.html similarity index 100% rename from selfdrive/assets/offroad/fcc.html rename to openpilot/selfdrive/assets/offroad/fcc.html diff --git a/selfdrive/assets/offroad/mici_fcc.html b/openpilot/selfdrive/assets/offroad/mici_fcc.html similarity index 100% rename from selfdrive/assets/offroad/mici_fcc.html rename to openpilot/selfdrive/assets/offroad/mici_fcc.html diff --git a/selfdrive/assets/prep-svg.sh b/openpilot/selfdrive/assets/prep-svg.sh similarity index 100% rename from selfdrive/assets/prep-svg.sh rename to openpilot/selfdrive/assets/prep-svg.sh diff --git a/openpilot/selfdrive/assets/sounds/complete.wav b/openpilot/selfdrive/assets/sounds/complete.wav new file mode 100644 index 0000000000..f85d48f315 --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/complete.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3027da9834adf9c71177bc3086f8de3018ce22e4725b694d635723c5b3c860cd +size 97120 diff --git a/openpilot/selfdrive/assets/sounds/critical.wav b/openpilot/selfdrive/assets/sounds/critical.wav new file mode 100644 index 0000000000..f36aa1bacd --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/critical.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca29ff46fbd6d00cc02596de8c9abfbf1cacdc6a7b2f98b27a74d24c790da004 +size 52374 diff --git a/openpilot/selfdrive/assets/sounds/disengage.wav b/openpilot/selfdrive/assets/sounds/disengage.wav new file mode 100644 index 0000000000..7db1d31911 --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/disengage.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f47633b5082b911e79dd13fc2d1f4c2a9d44d2fe0860f3d390ffc4e3d772f7d +size 97120 diff --git a/selfdrive/assets/sounds/disengage_tizi.wav b/openpilot/selfdrive/assets/sounds/disengage_tizi.wav similarity index 100% rename from selfdrive/assets/sounds/disengage_tizi.wav rename to openpilot/selfdrive/assets/sounds/disengage_tizi.wav diff --git a/openpilot/selfdrive/assets/sounds/dm_critical.wav b/openpilot/selfdrive/assets/sounds/dm_critical.wav new file mode 100644 index 0000000000..9a4171444f --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/dm_critical.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:538271820d618046009bb456e930598f2736dd1a6174c11925ec3912581b05c1 +size 52374 diff --git a/openpilot/selfdrive/assets/sounds/dm_warning.wav b/openpilot/selfdrive/assets/sounds/dm_warning.wav new file mode 100644 index 0000000000..52124b0801 --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/dm_warning.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:634fad6590295ef2e73d5f7e5aecbb5dd245a0f7692f1e300c2ebac95aabb8e3 +size 73026 diff --git a/openpilot/selfdrive/assets/sounds/engage.wav b/openpilot/selfdrive/assets/sounds/engage.wav new file mode 100644 index 0000000000..a984e2f3da --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/engage.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a380fe0a022856b302841a0dd19b71eab318c6b08ec851f128942758a7f1631a +size 97120 diff --git a/selfdrive/assets/sounds/engage_tizi.wav b/openpilot/selfdrive/assets/sounds/engage_tizi.wav similarity index 100% rename from selfdrive/assets/sounds/engage_tizi.wav rename to openpilot/selfdrive/assets/sounds/engage_tizi.wav diff --git a/selfdrive/assets/sounds/make_beeps.py b/openpilot/selfdrive/assets/sounds/make_beeps.py similarity index 100% rename from selfdrive/assets/sounds/make_beeps.py rename to openpilot/selfdrive/assets/sounds/make_beeps.py diff --git a/openpilot/selfdrive/assets/sounds/pre_alert.wav b/openpilot/selfdrive/assets/sounds/pre_alert.wav new file mode 100644 index 0000000000..d8240a1717 --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/pre_alert.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc9e67cfaba77e8e4f4049f0add9238660e4cf8f886c6915600d8f47a30c98db +size 97120 diff --git a/selfdrive/assets/sounds/prompt_single_high.wav b/openpilot/selfdrive/assets/sounds/prompt_single_high.wav similarity index 100% rename from selfdrive/assets/sounds/prompt_single_high.wav rename to openpilot/selfdrive/assets/sounds/prompt_single_high.wav diff --git a/selfdrive/assets/sounds/prompt_single_low.wav b/openpilot/selfdrive/assets/sounds/prompt_single_low.wav similarity index 100% rename from selfdrive/assets/sounds/prompt_single_low.wav rename to openpilot/selfdrive/assets/sounds/prompt_single_low.wav diff --git a/openpilot/selfdrive/assets/sounds/refuse.wav b/openpilot/selfdrive/assets/sounds/refuse.wav new file mode 100644 index 0000000000..10026d7be9 --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/refuse.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:081d64ca28a84a59ff5e6aea4732865b8687585938b8a287bc7e6a49d0646508 +size 97120 diff --git a/openpilot/selfdrive/assets/sounds/warning.wav b/openpilot/selfdrive/assets/sounds/warning.wav new file mode 100644 index 0000000000..bb71a0176a --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/warning.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bb440f424c989c06e203bfba0b32d39aede8f6c78a9b335e5e778460202b601 +size 73026 diff --git a/selfdrive/assets/training/step0.png b/openpilot/selfdrive/assets/training/step0.png similarity index 100% rename from selfdrive/assets/training/step0.png rename to openpilot/selfdrive/assets/training/step0.png diff --git a/selfdrive/assets/training/step1.png b/openpilot/selfdrive/assets/training/step1.png similarity index 100% rename from selfdrive/assets/training/step1.png rename to openpilot/selfdrive/assets/training/step1.png diff --git a/selfdrive/assets/training/step10.png b/openpilot/selfdrive/assets/training/step10.png similarity index 100% rename from selfdrive/assets/training/step10.png rename to openpilot/selfdrive/assets/training/step10.png diff --git a/selfdrive/assets/training/step11.png b/openpilot/selfdrive/assets/training/step11.png similarity index 100% rename from selfdrive/assets/training/step11.png rename to openpilot/selfdrive/assets/training/step11.png diff --git a/selfdrive/assets/training/step12.png b/openpilot/selfdrive/assets/training/step12.png similarity index 100% rename from selfdrive/assets/training/step12.png rename to openpilot/selfdrive/assets/training/step12.png diff --git a/selfdrive/assets/training/step13.png b/openpilot/selfdrive/assets/training/step13.png similarity index 100% rename from selfdrive/assets/training/step13.png rename to openpilot/selfdrive/assets/training/step13.png diff --git a/selfdrive/assets/training/step14.png b/openpilot/selfdrive/assets/training/step14.png similarity index 100% rename from selfdrive/assets/training/step14.png rename to openpilot/selfdrive/assets/training/step14.png diff --git a/selfdrive/assets/training/step15.png b/openpilot/selfdrive/assets/training/step15.png similarity index 100% rename from selfdrive/assets/training/step15.png rename to openpilot/selfdrive/assets/training/step15.png diff --git a/selfdrive/assets/training/step16.png b/openpilot/selfdrive/assets/training/step16.png similarity index 100% rename from selfdrive/assets/training/step16.png rename to openpilot/selfdrive/assets/training/step16.png diff --git a/selfdrive/assets/training/step17.png b/openpilot/selfdrive/assets/training/step17.png similarity index 100% rename from selfdrive/assets/training/step17.png rename to openpilot/selfdrive/assets/training/step17.png diff --git a/selfdrive/assets/training/step18.png b/openpilot/selfdrive/assets/training/step18.png similarity index 100% rename from selfdrive/assets/training/step18.png rename to openpilot/selfdrive/assets/training/step18.png diff --git a/selfdrive/assets/training/step2.png b/openpilot/selfdrive/assets/training/step2.png similarity index 100% rename from selfdrive/assets/training/step2.png rename to openpilot/selfdrive/assets/training/step2.png diff --git a/selfdrive/assets/training/step3.png b/openpilot/selfdrive/assets/training/step3.png similarity index 100% rename from selfdrive/assets/training/step3.png rename to openpilot/selfdrive/assets/training/step3.png diff --git a/selfdrive/assets/training/step4.png b/openpilot/selfdrive/assets/training/step4.png similarity index 100% rename from selfdrive/assets/training/step4.png rename to openpilot/selfdrive/assets/training/step4.png diff --git a/selfdrive/assets/training/step5.png b/openpilot/selfdrive/assets/training/step5.png similarity index 100% rename from selfdrive/assets/training/step5.png rename to openpilot/selfdrive/assets/training/step5.png diff --git a/selfdrive/assets/training/step6.png b/openpilot/selfdrive/assets/training/step6.png similarity index 100% rename from selfdrive/assets/training/step6.png rename to openpilot/selfdrive/assets/training/step6.png diff --git a/selfdrive/assets/training/step7.png b/openpilot/selfdrive/assets/training/step7.png similarity index 100% rename from selfdrive/assets/training/step7.png rename to openpilot/selfdrive/assets/training/step7.png diff --git a/selfdrive/assets/training/step8.png b/openpilot/selfdrive/assets/training/step8.png similarity index 100% rename from selfdrive/assets/training/step8.png rename to openpilot/selfdrive/assets/training/step8.png diff --git a/selfdrive/assets/training/step9.png b/openpilot/selfdrive/assets/training/step9.png similarity index 100% rename from selfdrive/assets/training/step9.png rename to openpilot/selfdrive/assets/training/step9.png diff --git a/selfdrive/car/CARS_template.md b/openpilot/selfdrive/car/CARS_template.md similarity index 98% rename from selfdrive/car/CARS_template.md rename to openpilot/selfdrive/car/CARS_template.md index bc335b6bd3..35804ff1fa 100644 --- a/selfdrive/car/CARS_template.md +++ b/openpilot/selfdrive/car/CARS_template.md @@ -6,7 +6,7 @@ {% set hardware_col_name = 'Hardware Needed' %} {% set wide_hardware_col_name = width_tag|format(hardware_col_name) -%} - + # Supported Cars diff --git a/selfdrive/car/tests/__init__.py b/openpilot/selfdrive/car/__init__.py similarity index 100% rename from selfdrive/car/tests/__init__.py rename to openpilot/selfdrive/car/__init__.py diff --git a/selfdrive/car/car_specific.py b/openpilot/selfdrive/car/car_specific.py similarity index 99% rename from selfdrive/car/car_specific.py rename to openpilot/selfdrive/car/car_specific.py index baa29978e3..244a8e3b07 100644 --- a/selfdrive/car/car_specific.py +++ b/openpilot/selfdrive/car/car_specific.py @@ -1,4 +1,5 @@ -from cereal import car, log +from openpilot.cereal import log +from opendbc.car.structs import car from opendbc.car import DT_CTRL, structs from opendbc.car.car_helpers import interfaces from opendbc.car.interfaces import MAX_CTRL_SPEED diff --git a/selfdrive/car/card.py b/openpilot/selfdrive/car/card.py similarity index 99% rename from selfdrive/car/card.py rename to openpilot/selfdrive/car/card.py index 995d02aabc..d9b7b4bb5a 100755 --- a/selfdrive/car/card.py +++ b/openpilot/selfdrive/car/card.py @@ -3,9 +3,10 @@ import os import time import threading -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging -from cereal import car, log, custom +from openpilot.cereal import log, custom +from opendbc.car.structs import car from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper diff --git a/selfdrive/car/cruise.py b/openpilot/selfdrive/car/cruise.py similarity index 99% rename from selfdrive/car/cruise.py rename to openpilot/selfdrive/car/cruise.py index 572dfabc02..b554c1a978 100644 --- a/selfdrive/car/cruise.py +++ b/openpilot/selfdrive/car/cruise.py @@ -1,7 +1,7 @@ import math import numpy as np -from cereal import car +from opendbc.car.structs import car from openpilot.common.constants import CV from openpilot.sunnypilot.selfdrive.car.cruise_ext import VCruiseHelperSP diff --git a/selfdrive/car/docs.py b/openpilot/selfdrive/car/docs.py similarity index 89% rename from selfdrive/car/docs.py rename to openpilot/selfdrive/car/docs.py index f807fc320e..ea7a70688e 100755 --- a/selfdrive/car/docs.py +++ b/openpilot/selfdrive/car/docs.py @@ -6,7 +6,7 @@ from openpilot.common.basedir import BASEDIR from opendbc.car.docs import get_all_car_docs, generate_cars_md CARS_MD_OUT = os.path.join(BASEDIR, "docs", "CARS.md") -CARS_MD_TEMPLATE = os.path.join(BASEDIR, "selfdrive", "car", "CARS_template.md") +CARS_MD_TEMPLATE = os.path.join(BASEDIR, "openpilot/selfdrive", "car", "CARS_template.md") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Auto generates supported cars documentation", diff --git a/selfdrive/car/helpers.py b/openpilot/selfdrive/car/helpers.py similarity index 98% rename from selfdrive/car/helpers.py rename to openpilot/selfdrive/car/helpers.py index 384152c71b..ec9e3efeeb 100644 --- a/selfdrive/car/helpers.py +++ b/openpilot/selfdrive/car/helpers.py @@ -1,7 +1,7 @@ import capnp from typing import Any -from cereal import custom +from openpilot.cereal import custom from opendbc.car import structs _FIELDS = '__dataclass_fields__' # copy of dataclasses._FIELDS diff --git a/selfdrive/controls/__init__.py b/openpilot/selfdrive/car/tests/__init__.py similarity index 100% rename from selfdrive/controls/__init__.py rename to openpilot/selfdrive/car/tests/__init__.py diff --git a/openpilot/selfdrive/car/tests/big_cars_test.sh b/openpilot/selfdrive/car/tests/big_cars_test.sh new file mode 100755 index 0000000000..456fc698c5 --- /dev/null +++ b/openpilot/selfdrive/car/tests/big_cars_test.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +SCRIPT_DIR=$(dirname "$0") +BASEDIR=$(realpath "$SCRIPT_DIR/../../../../") +cd $BASEDIR + +export MAX_EXAMPLES=300 +export INTERNAL_SEG_CNT=300 +export INTERNAL_SEG_LIST=openpilot/selfdrive/car/tests/test_models_segs.txt + +cd openpilot/selfdrive/car/tests && pytest test_models.py test_car_interfaces.py diff --git a/selfdrive/car/tests/test_car_interfaces.py b/openpilot/selfdrive/car/tests/test_car_interfaces.py similarity index 97% rename from selfdrive/car/tests/test_car_interfaces.py rename to openpilot/selfdrive/car/tests/test_car_interfaces.py index cd44759cbf..3c4587cbc6 100644 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/openpilot/selfdrive/car/tests/test_car_interfaces.py @@ -3,7 +3,8 @@ import hypothesis.strategies as st from hypothesis import Phase, given, settings from openpilot.common.parameterized import parameterized -from cereal import car, custom +from openpilot.cereal import custom +from opendbc.car.structs import car from opendbc.car import DT_CTRL from opendbc.car.structs import CarParams from opendbc.car.tests.test_car_interfaces import get_fuzzy_car_interface diff --git a/selfdrive/car/tests/test_cruise_speed.py b/openpilot/selfdrive/car/tests/test_cruise_speed.py similarity index 98% rename from selfdrive/car/tests/test_cruise_speed.py rename to openpilot/selfdrive/car/tests/test_cruise_speed.py index 360c93c25b..c0b6ac979f 100644 --- a/selfdrive/car/tests/test_cruise_speed.py +++ b/openpilot/selfdrive/car/tests/test_cruise_speed.py @@ -3,9 +3,10 @@ import itertools import numpy as np from openpilot.common.parameterized import parameterized_class -from cereal import log +from openpilot.cereal import log from openpilot.selfdrive.car.cruise import VCruiseHelper, V_CRUISE_MIN, V_CRUISE_MAX, V_CRUISE_INITIAL, IMPERIAL_INCREMENT -from cereal import car, custom +from openpilot.cereal import custom +from opendbc.car.structs import car from openpilot.common.constants import CV from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver diff --git a/openpilot/selfdrive/car/tests/test_docs.py b/openpilot/selfdrive/car/tests/test_docs.py new file mode 100644 index 0000000000..8ccbfb5a79 --- /dev/null +++ b/openpilot/selfdrive/car/tests/test_docs.py @@ -0,0 +1,11 @@ +from opendbc.car.docs import generate_cars_md, get_all_car_docs +from openpilot.selfdrive.car.docs import CARS_MD_TEMPLATE + + +class TestCarDocs: + @classmethod + def setup_class(cls): + cls.all_cars = get_all_car_docs() + + def test_generator(self): + generate_cars_md(self.all_cars, CARS_MD_TEMPLATE) diff --git a/selfdrive/car/tests/test_models.py b/openpilot/selfdrive/car/tests/test_models.py similarity index 92% rename from selfdrive/car/tests/test_models.py rename to openpilot/selfdrive/car/tests/test_models.py index a99e8b9d1d..b98890838f 100644 --- a/selfdrive/car/tests/test_models.py +++ b/openpilot/selfdrive/car/tests/test_models.py @@ -3,17 +3,16 @@ import copy import os import pytest import random -import unittest # noqa: TID251 +import unittest from collections import defaultdict, Counter import hypothesis.strategies as st from hypothesis import Phase, given, settings from openpilot.common.parameterized import parameterized_class - from opendbc.car import DT_CTRL, gen_empty_fingerprint, structs from opendbc.car.can_definitions import CanData from opendbc.car.car_helpers import FRAME_FINGERPRINT, interfaces from opendbc.car.fingerprints import MIGRATION -from opendbc.car.honda.values import HondaFlags +from opendbc.car.honda.values import CAR as HONDA, HondaFlags from opendbc.car.structs import car from opendbc.car.tests.routes import non_tested_cars, routes, CarTestRoute from opendbc.car.values import Platform, PLATFORMS @@ -21,8 +20,9 @@ from opendbc.safety.tests.libsafety import libsafety_py from openpilot.common.basedir import BASEDIR from openpilot.selfdrive.pandad import can_capnp_to_list from openpilot.selfdrive.test.helpers import read_segment_list -from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT +from openpilot.common.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotci_source, internal_source, comma_api_source +from openpilot.tools.lib.file_sources import Source from openpilot.tools.lib.route import SegmentName SafetyModel = car.CarParams.SafetyModel @@ -132,7 +132,7 @@ class TestCarModelBase(unittest.TestCase): segment_range = f"{cls.test_route.route}/{seg}" try: - sources = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source] + sources: list[Source] = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source] lr = LogReader(segment_range, sources=sources, sort_by_time=True) return cls.get_testing_data_from_logreader(lr) except (LogsUnavailable, AssertionError): @@ -192,7 +192,7 @@ class TestCarModelBase(unittest.TestCase): # make sure car params are within a valid range self.assertGreater(self.CP.mass, 1) - if self.CP.steerControlType != SteerControlType.angle: + if self.CP.steerControlType not in (SteerControlType.angle, SteerControlType.curvature): tuning = self.CP.lateralTuning.which() if tuning == 'pid': self.assertTrue(len(self.CP.lateralTuning.pid.kpV)) @@ -258,7 +258,7 @@ class TestCarModelBase(unittest.TestCase): # Don't check relay malfunction on disabled routes (relay closed), # or before fingerprinting is done (elm327 and noOutput) - if self.openpilot_enabled and t / 1e4 > self.car_safety_mode_frame: + if self.car_safety_mode_frame is not None and t / 1e4 > self.car_safety_mode_frame: self.assertFalse(self.safety.get_relay_malfunction()) else: self.safety.set_relay_malfunction(False) @@ -358,7 +358,13 @@ class TestCarModelBase(unittest.TestCase): self.assertEqual(CS.gasPressed, self.safety.get_gas_pressed_prev()) if self.safety.get_brake_pressed_prev() != prev_panda_brake: - self.assertEqual(CS.brakePressed, self.safety.get_brake_pressed_prev()) + # TODO: remove this exception once this mismatch is resolved + brake_pressed = CS.brakePressed + if CS.brakePressed and not self.safety.get_brake_pressed_prev(): + if self.CP.carFingerprint in (HONDA.HONDA_PILOT, HONDA.HONDA_RIDGELINE) and CS.brake > 0.05: + brake_pressed = False + + self.assertEqual(brake_pressed, self.safety.get_brake_pressed_prev()) if self.safety.get_regen_braking_prev() != prev_panda_regen_braking: self.assertEqual(CS.regenBraking, self.safety.get_regen_braking_prev()) @@ -436,13 +442,19 @@ class TestCarModelBase(unittest.TestCase): v_ego_raw < (self.safety.get_vehicle_speed_min() - 1e-3)) # check steering angle for angle control cars (panda stores angle_meas in CAN units) - # ford excluded since it tracks curvature, not steering angle - if self.CP.steerControlType == SteerControlType.angle and not self.CP.notCar and self.CP.brand != "ford": + # ford and VW MEB excluded since they track curvature, not steering angle + # TODO: add curvature check, standardize CAN units to rm brand specific ANGLE_DEG_TO_CAN + if self.CP.steerControlType == SteerControlType.angle and not self.CP.notCar and self.CP.brand not in ("ford", "volkswagen"): angle_can = (CS.steeringAngleDeg + CS.steeringAngleOffsetDeg) * ANGLE_DEG_TO_CAN[self.CP.brand] checks['steeringAngleDeg'] += (angle_can > (self.safety.get_angle_meas_max() + 1) or angle_can < (self.safety.get_angle_meas_min() - 1)) - checks['brakePressed'] += CS.brakePressed != self.safety.get_brake_pressed_prev() + # TODO: remove this exception once this mismatch is resolved + brake_pressed = CS.brakePressed + if CS.brakePressed and not self.safety.get_brake_pressed_prev(): + if self.CP.carFingerprint in (HONDA.HONDA_PILOT, HONDA.HONDA_RIDGELINE) and CS.deprecated.brake > 0.05: + brake_pressed = False + checks['brakePressed'] += brake_pressed != self.safety.get_brake_pressed_prev() checks['regenBraking'] += CS.regenBraking != self.safety.get_regen_braking_prev() checks['steeringDisengage'] += CS.steeringDisengage != self.safety.get_steering_disengage_prev() diff --git a/selfdrive/car/tests/test_models_segs.txt b/openpilot/selfdrive/car/tests/test_models_segs.txt similarity index 100% rename from selfdrive/car/tests/test_models_segs.txt rename to openpilot/selfdrive/car/tests/test_models_segs.txt diff --git a/selfdrive/controls/lib/__init__.py b/openpilot/selfdrive/controls/__init__.py similarity index 100% rename from selfdrive/controls/lib/__init__.py rename to openpilot/selfdrive/controls/__init__.py diff --git a/selfdrive/controls/controlsd.py b/openpilot/selfdrive/controls/controlsd.py similarity index 89% rename from selfdrive/controls/controlsd.py rename to openpilot/selfdrive/controls/controlsd.py index c242871125..d9c0b445d8 100755 --- a/selfdrive/controls/controlsd.py +++ b/openpilot/selfdrive/controls/controlsd.py @@ -2,8 +2,9 @@ import math from numbers import Number -from cereal import car, log -import cereal.messaging as messaging +from openpilot.cereal import log +from opendbc.car.structs import car +import openpilot.cereal.messaging as messaging from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process, DT_CTRL, Priority, Ratekeeper @@ -15,6 +16,7 @@ from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature from openpilot.selfdrive.controls.lib.latcontrol import LatControl from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD +from openpilot.selfdrive.controls.lib.latcontrol_curvature import LatControlCurvature from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.selfdrive.controls.lib.longcontrol import LongControl from openpilot.selfdrive.modeld.modeld import LAT_SMOOTH_SECONDS @@ -59,6 +61,8 @@ class Controls(ControlsExt): self.LaC: LatControl if self.CP.steerControlType == car.CarParams.SteerControlType.angle: self.LaC = LatControlAngle(self.CP, self.CP_SP, self.CI, DT_CTRL) + elif self.CP.steerControlType == car.CarParams.SteerControlType.curvature: + self.LaC = LatControlCurvature(self.CP, self.CP_SP, self.CI, DT_CTRL) elif self.CP.lateralTuning.which() == 'pid': self.LaC = LatControlPID(self.CP, self.CP_SP, self.CI, DT_CTRL) elif self.CP.lateralTuning.which() == 'torque': @@ -143,11 +147,14 @@ class Controls(ControlsExt): lat_delay = self.sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS actuators.curvature = self.desired_curvature - steer, steeringAngleDeg, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp, - self.steer_limited_by_safety, self.desired_curvature, - self.calibrated_pose, curvature_limited, lat_delay) + steer, lateral_output, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp, + self.steer_limited_by_safety, self.desired_curvature, + self.calibrated_pose, curvature_limited, lat_delay) actuators.torque = float(steer) - actuators.steeringAngleDeg = float(steeringAngleDeg) + if self.CP.steerControlType == car.CarParams.SteerControlType.curvature: + actuators.curvature = float(lateral_output) + else: + actuators.steeringAngleDeg = float(lateral_output) # Ensure no NaNs/Infs for p in ACTUATOR_FIELDS: attr = getattr(actuators, p) @@ -212,12 +219,17 @@ class Controls(ControlsExt): cs.upAccelCmd = float(self.LoC.pid.p) cs.uiAccelCmd = float(self.LoC.pid.i) cs.ufAccelCmd = float(self.LoC.pid.f) - cs.forceDecel = bool((self.sm['driverMonitoringState'].alertLevel == log.DriverMonitoringState.AlertLevel.three) or + cs.forceDecel = bool(self.sm['driverMonitoringState'].noResponseForceDecel or (self.sm['selfdriveState'].state == State.softDisabling)) + # trigger the car's stock driver monitoring escalation + CC.driverMonitoringEscalation = cs.forceDecel + lat_tuning = self.CP.lateralTuning.which() if self.CP.steerControlType == car.CarParams.SteerControlType.angle: cs.lateralControlState.angleState = lac_log + elif self.CP.steerControlType == car.CarParams.SteerControlType.curvature: + cs.lateralControlState.curvatureState = lac_log elif lat_tuning == 'pid': cs.lateralControlState.pidState = lac_log elif lat_tuning == 'torque': diff --git a/selfdrive/controls/lib/lateral_mpc_lib/__init__.py b/openpilot/selfdrive/controls/lib/__init__.py similarity index 100% rename from selfdrive/controls/lib/lateral_mpc_lib/__init__.py rename to openpilot/selfdrive/controls/lib/__init__.py diff --git a/selfdrive/controls/lib/desire_helper.py b/openpilot/selfdrive/controls/lib/desire_helper.py similarity index 62% rename from selfdrive/controls/lib/desire_helper.py rename to openpilot/selfdrive/controls/lib/desire_helper.py index 16908fa3e3..df4e5c56ab 100644 --- a/selfdrive/controls/lib/desire_helper.py +++ b/openpilot/selfdrive/controls/lib/desire_helper.py @@ -1,4 +1,4 @@ -from cereal import log, custom +from openpilot.cereal import log, custom from openpilot.common.constants import CV from openpilot.common.realtime import DT_MDL from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode @@ -10,27 +10,7 @@ TurnDirection = custom.ModelDataV2SP.TurnDirection LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS LANE_CHANGE_TIME_MAX = 10. - -DESIRES = { - LaneChangeDirection.none: { - LaneChangeState.off: log.Desire.none, - LaneChangeState.preLaneChange: log.Desire.none, - LaneChangeState.laneChangeStarting: log.Desire.none, - LaneChangeState.laneChangeFinishing: log.Desire.none, - }, - LaneChangeDirection.left: { - LaneChangeState.off: log.Desire.none, - LaneChangeState.preLaneChange: log.Desire.none, - LaneChangeState.laneChangeStarting: log.Desire.laneChangeLeft, - LaneChangeState.laneChangeFinishing: log.Desire.laneChangeLeft, - }, - LaneChangeDirection.right: { - LaneChangeState.off: log.Desire.none, - LaneChangeState.preLaneChange: log.Desire.none, - LaneChangeState.laneChangeStarting: log.Desire.laneChangeRight, - LaneChangeState.laneChangeFinishing: log.Desire.laneChangeRight, - }, -} +LANE_CHANGE_START_TIME = 0.5 TURN_DESIRES = { TurnDirection.none: log.Desire.none, @@ -38,14 +18,11 @@ TURN_DESIRES = { TurnDirection.turnRight: log.Desire.turnRight, } - class DesireHelper: def __init__(self): self.lane_change_state = LaneChangeState.off self.lane_change_direction = LaneChangeDirection.none self.lane_change_timer = 0.0 - self.lane_change_ll_prob = 1.0 - self.keep_pulse_timer = 0.0 self.prev_one_blinker = False self.desire = log.Desire.none self.alc = AutoLaneChangeController(self) @@ -71,15 +48,14 @@ class DesireHelper: if not lateral_active or self.lane_change_timer > LANE_CHANGE_TIME_MAX or self.alc.lane_change_set_timer == AutoLaneChangeMode.OFF: self.lane_change_state = LaneChangeState.off self.lane_change_direction = LaneChangeDirection.none + self.lane_change_timer = 0.0 else: - # LaneChangeState.off if self.lane_change_state == LaneChangeState.off and one_blinker and not self.prev_one_blinker and not below_lane_change_speed: self.lane_change_state = LaneChangeState.preLaneChange - self.lane_change_ll_prob = 1.0 + self.lane_change_timer = 0.0 # Initialize lane change direction to prevent UI alert flicker self.lane_change_direction = self.get_lane_change_direction(carstate) - # LaneChangeState.preLaneChange elif self.lane_change_state == LaneChangeState.preLaneChange: # Update lane change direction self.lane_change_direction = self.get_lane_change_direction(carstate) @@ -96,50 +72,33 @@ class DesireHelper: if not one_blinker or below_lane_change_speed: self.lane_change_state = LaneChangeState.off self.lane_change_direction = LaneChangeDirection.none + self.lane_change_timer = 0.0 elif (torque_applied or self.alc.auto_lane_change_allowed) and not blindspot_detected: self.lane_change_state = LaneChangeState.laneChangeStarting + self.lane_change_timer = 0.0 - # LaneChangeState.laneChangeStarting elif self.lane_change_state == LaneChangeState.laneChangeStarting: - # fade out over .5s - self.lane_change_ll_prob = max(self.lane_change_ll_prob - 2 * DT_MDL, 0.0) + self.lane_change_timer += DT_MDL - # 98% certainty - if lane_change_prob < 0.02 and self.lane_change_ll_prob < 0.01: - self.lane_change_state = LaneChangeState.laneChangeFinishing - - # LaneChangeState.laneChangeFinishing - elif self.lane_change_state == LaneChangeState.laneChangeFinishing: - # fade in laneline over 1s - self.lane_change_ll_prob = min(self.lane_change_ll_prob + DT_MDL, 1.0) - - if self.lane_change_ll_prob > 0.99: - self.lane_change_direction = LaneChangeDirection.none + if lane_change_prob < 0.02 and self.lane_change_timer >= LANE_CHANGE_START_TIME: + self.lane_change_timer = 0.0 if one_blinker: self.lane_change_state = LaneChangeState.preLaneChange + self.lane_change_direction = self.get_lane_change_direction(carstate) else: self.lane_change_state = LaneChangeState.off + self.lane_change_direction = LaneChangeDirection.none - if self.lane_change_state in (LaneChangeState.off, LaneChangeState.preLaneChange): - self.lane_change_timer = 0.0 - else: - self.lane_change_timer += DT_MDL - - self.prev_one_blinker = one_blinker + self.prev_one_blinker = one_blinker and lateral_active if self.lane_turn_direction != TurnDirection.none: self.desire = TURN_DESIRES[self.lane_turn_direction] else: - self.desire = DESIRES[self.lane_change_direction][self.lane_change_state] - - # Send keep pulse once per second during LaneChangeStart.preLaneChange - if self.lane_change_state in (LaneChangeState.off, LaneChangeState.laneChangeStarting): - self.keep_pulse_timer = 0.0 - elif self.lane_change_state == LaneChangeState.preLaneChange: - self.keep_pulse_timer += DT_MDL - if self.keep_pulse_timer > 1.0: - self.keep_pulse_timer = 0.0 - elif self.desire in (log.Desire.keepLeft, log.Desire.keepRight): - self.desire = log.Desire.none + self.desire = log.Desire.none + if self.lane_change_state == LaneChangeState.laneChangeStarting: + if self.lane_change_direction == LaneChangeDirection.left: + self.desire = log.Desire.laneChangeLeft + elif self.lane_change_direction == LaneChangeDirection.right: + self.desire = log.Desire.laneChangeRight self.alc.update_state() diff --git a/selfdrive/controls/lib/drive_helpers.py b/openpilot/selfdrive/controls/lib/drive_helpers.py similarity index 92% rename from selfdrive/controls/lib/drive_helpers.py rename to openpilot/selfdrive/controls/lib/drive_helpers.py index 5392ff8875..497f8d1bdf 100644 --- a/selfdrive/controls/lib/drive_helpers.py +++ b/openpilot/selfdrive/controls/lib/drive_helpers.py @@ -15,6 +15,9 @@ MAX_LATERAL_JERK = 5.0 # m/s^3 MAX_LATERAL_ACCEL_NO_ROLL = 3.0 # m/s^2 +def should_stop(v_ego: float, a_target: float) -> bool: + return bool(v_ego < 0.25 and a_target < 0.1) + def clamp(val, min_val, max_val): clamped_val = float(np.clip(val, min_val, max_val)) return clamped_val, clamped_val != val @@ -40,7 +43,7 @@ def clip_curvature(v_ego, prev_curvature, new_curvature, roll) -> tuple[float, b return float(new_curvature), limited_accel or limited_max_curv -def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0.3): +def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL): if len(speeds) == len(t_idxs): v_now = speeds[0] a_now = accels[0] @@ -53,8 +56,7 @@ def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0. v_now = 0.0 v_target = 0.0 a_target = 0.0 - should_stop = (v_now < vEgoStopping and a_target < 0.1) - return a_target, should_stop + return a_target, should_stop(v_now, a_target) def curv_from_psis(psi_target, psi_rate, vego, action_t): vego = np.clip(vego, MIN_SPEED, np.inf) diff --git a/selfdrive/controls/lib/latcontrol.py b/openpilot/selfdrive/controls/lib/latcontrol.py similarity index 93% rename from selfdrive/controls/lib/latcontrol.py rename to openpilot/selfdrive/controls/lib/latcontrol.py index 4207a188f4..5519d37296 100644 --- a/selfdrive/controls/lib/latcontrol.py +++ b/openpilot/selfdrive/controls/lib/latcontrol.py @@ -14,7 +14,7 @@ class LatControl(ABC): self.steer_max = 1.0 @abstractmethod - def update(self, active: bool, CS, VM, params, steer_limited_by_safety: bool, desired_curvature: float, calibrated_pose: Pose, + def update(self, active: bool, CS, VM, params, steer_limited_by_safety: bool, desired_curvature: float, calibrated_pose: Pose | None, curvature_limited: bool, lat_delay: float): pass diff --git a/selfdrive/controls/lib/latcontrol_angle.py b/openpilot/selfdrive/controls/lib/latcontrol_angle.py similarity index 98% rename from selfdrive/controls/lib/latcontrol_angle.py rename to openpilot/selfdrive/controls/lib/latcontrol_angle.py index 9aa5b3cd01..18ccbae57b 100644 --- a/selfdrive/controls/lib/latcontrol_angle.py +++ b/openpilot/selfdrive/controls/lib/latcontrol_angle.py @@ -1,6 +1,6 @@ import math -from cereal import log +from openpilot.cereal import log from openpilot.selfdrive.controls.lib.latcontrol import LatControl # TODO This is speed dependent diff --git a/openpilot/selfdrive/controls/lib/latcontrol_curvature.py b/openpilot/selfdrive/controls/lib/latcontrol_curvature.py new file mode 100644 index 0000000000..3dfe691e10 --- /dev/null +++ b/openpilot/selfdrive/controls/lib/latcontrol_curvature.py @@ -0,0 +1,58 @@ +import math + +from openpilot.cereal import log +from openpilot.common.pid import PIDController +from openpilot.selfdrive.controls.lib.latcontrol import LatControl +from openpilot.selfdrive.controls.lib.drive_helpers import MAX_CURVATURE + +CURVATURE_SATURATION_THRESHOLD = 1e-3 # 1/m + + +class LatControlCurvature(LatControl): + def __init__(self, CP, CP_SP, CI, dt): + super().__init__(CP, CP_SP, CI, dt) + self.sat_check_min_speed = 5. + if CP.lateralTuning.which() == 'pid': + ct = CP.lateralTuning.pid + self.pid = PIDController((ct.kpBP, ct.kpV), (ct.kiBP, ct.kiV), + pos_limit=MAX_CURVATURE, neg_limit=-MAX_CURVATURE, rate=1 / dt) + self.kf = ct.kf + else: + self.pid = None + self.kf = 1. + + def reset(self): + super().reset() + if self.pid is not None: + self.pid.reset() + + def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay): + curvature_log = log.ControlsState.LateralCurvatureState.new_message() + actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) + error = desired_curvature - actual_curvature + + if not active: + output_curvature = 0.0 + curvature_log.active = False + if self.pid is not None: + self.pid.reset() + elif self.pid is None or CS.steeringPressed: + # no PID or override: feedforward only + if self.pid is not None: + self.pid.reset() + output_curvature = self.kf * desired_curvature + curvature_log.active = True + else: + output_curvature = self.pid.update(error, speed=CS.vEgo, feedforward=self.kf * desired_curvature) + curvature_log.p = float(self.pid.p) + curvature_log.i = float(self.pid.i) + curvature_log.f = float(self.pid.f) + curvature_log.active = True + + curvature_log.error = float(error) + curvature_log.actualCurvature = float(actual_curvature) + curvature_log.desiredCurvature = float(desired_curvature) + curvature_log.output = float(output_curvature) + curvature_log.saturated = bool(self._check_saturation(abs(error) > CURVATURE_SATURATION_THRESHOLD, CS, + False, curvature_limited)) + return 0.0, float(output_curvature), curvature_log diff --git a/selfdrive/controls/lib/latcontrol_pid.py b/openpilot/selfdrive/controls/lib/latcontrol_pid.py similarity index 98% rename from selfdrive/controls/lib/latcontrol_pid.py rename to openpilot/selfdrive/controls/lib/latcontrol_pid.py index 25b2c8d87e..214f4724d1 100644 --- a/selfdrive/controls/lib/latcontrol_pid.py +++ b/openpilot/selfdrive/controls/lib/latcontrol_pid.py @@ -1,6 +1,6 @@ import math -from cereal import log +from openpilot.cereal import log from openpilot.selfdrive.controls.lib.latcontrol import LatControl from openpilot.common.pid import PIDController diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/openpilot/selfdrive/controls/lib/latcontrol_torque.py similarity index 99% rename from selfdrive/controls/lib/latcontrol_torque.py rename to openpilot/selfdrive/controls/lib/latcontrol_torque.py index d0eb40d096..952234de99 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/openpilot/selfdrive/controls/lib/latcontrol_torque.py @@ -2,7 +2,7 @@ import math import numpy as np from collections import deque -from cereal import log +from openpilot.cereal import log from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.common.filter_simple import FirstOrderFilter diff --git a/selfdrive/controls/lib/ldw.py b/openpilot/selfdrive/controls/lib/ldw.py similarity index 97% rename from selfdrive/controls/lib/ldw.py rename to openpilot/selfdrive/controls/lib/ldw.py index 78a6d6cf6e..6db59791cc 100644 --- a/selfdrive/controls/lib/ldw.py +++ b/openpilot/selfdrive/controls/lib/ldw.py @@ -1,4 +1,4 @@ -from cereal import log +from openpilot.cereal import log from openpilot.common.realtime import DT_CTRL from openpilot.common.constants import CV diff --git a/selfdrive/controls/lib/longcontrol.py b/openpilot/selfdrive/controls/lib/longcontrol.py similarity index 73% rename from selfdrive/controls/lib/longcontrol.py rename to openpilot/selfdrive/controls/lib/longcontrol.py index ec714f452e..e3b612e79f 100644 --- a/selfdrive/controls/lib/longcontrol.py +++ b/openpilot/selfdrive/controls/lib/longcontrol.py @@ -1,5 +1,5 @@ import numpy as np -from cereal import car +from opendbc.car.structs import car from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from openpilot.common.pid import PIDController @@ -10,16 +10,14 @@ CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N] LongCtrlState = car.CarControl.Actuators.LongControlState -def long_control_state_trans(CP, CP_SP, active, long_control_state, v_ego, +def long_control_state_trans(CP_SP, active, long_control_state, should_stop, brake_pressed, cruise_standstill): # Gas Interceptor cruise_standstill = cruise_standstill and not CP_SP.enableGasInterceptor - stopping_condition = should_stop starting_condition = (not should_stop and not cruise_standstill and not brake_pressed) - started_condition = v_ego > CP.vEgoStarting if not active: long_control_state = LongCtrlState.off @@ -29,22 +27,16 @@ def long_control_state_trans(CP, CP_SP, active, long_control_state, v_ego, if not starting_condition: long_control_state = LongCtrlState.stopping else: - if starting_condition and CP.startingState: - long_control_state = LongCtrlState.starting - else: - long_control_state = LongCtrlState.pid + long_control_state = LongCtrlState.pid elif long_control_state == LongCtrlState.stopping: - if starting_condition and CP.startingState: - long_control_state = LongCtrlState.starting - elif starting_condition: + if starting_condition: long_control_state = LongCtrlState.pid - elif long_control_state in [LongCtrlState.starting, LongCtrlState.pid]: - if stopping_condition: + elif long_control_state == LongCtrlState.pid: + if should_stop: long_control_state = LongCtrlState.stopping - elif started_condition: - long_control_state = LongCtrlState.pid + return long_control_state class LongControl: @@ -65,7 +57,7 @@ class LongControl: self.pid.neg_limit = accel_limits[0] self.pid.pos_limit = accel_limits[1] - self.long_control_state = long_control_state_trans(self.CP, self.CP_SP, active, self.long_control_state, CS.vEgo, + self.long_control_state = long_control_state_trans(self.CP_SP, active, self.long_control_state, should_stop, CS.brakePressed, CS.cruiseState.standstill) if self.long_control_state == LongCtrlState.off: @@ -76,11 +68,8 @@ class LongControl: output_accel = self.last_output_accel if output_accel > self.CP.stopAccel: output_accel = min(output_accel, 0.0) - output_accel -= self.CP.stoppingDecelRate * DT_CTRL - self.reset() - - elif self.long_control_state == LongCtrlState.starting: - output_accel = self.CP.startAccel + # TODO: can we just go straight to stopAccel? + output_accel -= 1.0 * DT_CTRL # m/s^2/s while trying to stop self.reset() else: # LongCtrlState.pid diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/.gitignore b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/.gitignore similarity index 100% rename from selfdrive/controls/lib/longitudinal_mpc_lib/.gitignore rename to openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/.gitignore diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript similarity index 98% rename from selfdrive/controls/lib/longitudinal_mpc_lib/SConscript rename to openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript index a3218e9f34..636ef0fb21 100644 --- a/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript +++ b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript @@ -55,7 +55,7 @@ acados_include_dir = Dir(acados.INCLUDE_DIR) acados_template_dir = Dir(acados.TEMPLATE_DIR) source_list = ['long_mpc.py', - '#selfdrive/modeld/constants.py', + '#openpilot/selfdrive/modeld/constants.py', acados_include_dir.File('acados_c/ocp_nlp_interface.h'), acados_template_dir.File('c_templates_tera/acados_solver.in.c'), ] diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/__init__.py b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/__init__.py similarity index 100% rename from selfdrive/controls/lib/longitudinal_mpc_lib/__init__.py rename to openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/__init__.py diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py similarity index 96% rename from selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py rename to openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py index deae416489..de1ba0c278 100755 --- a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py +++ b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @@ -2,7 +2,7 @@ import os import time import numpy as np -from cereal import log +from openpilot.cereal import log from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX from openpilot.common.realtime import DT_MDL from openpilot.common.swaglog import cloudlog @@ -240,14 +240,10 @@ class LongitudinalMpc: self.solver.set(i, 'x', np.zeros(X_DIM)) self.last_cloudlog_t = 0 - self.status = False self.crash_cnt = 0.0 self.solution_status = 0 # timers self.solve_time = 0.0 - self.time_qp_solution = 0.0 - self.time_linearization = 0.0 - self.time_integrator = 0.0 self.x0 = np.zeros(X_DIM) self.set_weights() @@ -292,7 +288,7 @@ class LongitudinalMpc: def process_lead(self, lead): v_ego = self.x0[1] - if lead is not None and lead.status: + if lead is not None and lead.present: x_lead = lead.dRel v_lead = lead.vLead a_lead = lead.aLeadK @@ -316,7 +312,6 @@ class LongitudinalMpc: 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 lead_xv_0 = self.process_lead(radarstate.leadOne) lead_xv_1 = self.process_lead(radarstate.leadTwo) @@ -365,9 +360,6 @@ class LongitudinalMpc: self.solution_status = self.solver.solve() self.solve_time = float(self.solver.get_stats('time_tot')[0]) - self.time_qp_solution = float(self.solver.get_stats('time_qp')[0]) - self.time_linearization = float(self.solver.get_stats('time_lin')[0]) - self.time_integrator = float(self.solver.get_stats('time_sim')[0]) for i in range(N+1): self.x_sol[i] = self.solver.get(i, 'x') diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/openpilot/selfdrive/controls/lib/longitudinal_planner.py similarity index 88% rename from selfdrive/controls/lib/longitudinal_planner.py rename to openpilot/selfdrive/controls/lib/longitudinal_planner.py index e02b02d2e0..6eaed7370d 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/openpilot/selfdrive/controls/lib/longitudinal_planner.py @@ -2,7 +2,7 @@ import math import numpy as np -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX from openpilot.common.constants import CV from openpilot.common.filter_simple import FirstOrderFilter @@ -66,26 +66,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP): self.a_desired_trajectory = np.zeros(CONTROL_N) self.j_desired_trajectory = np.zeros(CONTROL_N) - @staticmethod - def parse_model(model_msg): - if (len(model_msg.position.x) == ModelConstants.IDX_N and - len(model_msg.velocity.x) == ModelConstants.IDX_N and - len(model_msg.acceleration.x) == ModelConstants.IDX_N): - x = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.position.x) - v = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.velocity.x) - a = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.acceleration.x) - j = np.zeros(len(T_IDXS_MPC)) - else: - x = np.zeros(len(T_IDXS_MPC)) - v = np.zeros(len(T_IDXS_MPC)) - a = np.zeros(len(T_IDXS_MPC)) - j = np.zeros(len(T_IDXS_MPC)) - if len(model_msg.meta.disengagePredictions.gasPressProbs) > 1: - throttle_prob = model_msg.meta.disengagePredictions.gasPressProbs[1] - else: - throttle_prob = 1.0 - return x, v, a, j, throttle_prob - def update(self, sm): LongitudinalPlannerSP.update(self, sm) @@ -121,7 +101,8 @@ 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)) - _, _, _, _, throttle_prob = self.parse_model(sm['modelV2']) + throttle_probs = sm['modelV2'].meta.disengagePredictions.gasPressProbs + throttle_prob = throttle_probs[1] if len(throttle_probs) > 1 else 1.0 # 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 @@ -156,7 +137,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): action_t = self.CP.longitudinalActuatorDelay + DT_MDL output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX, - action_t=action_t, vEgoStopping=self.CP.vEgoStopping) + action_t=action_t) output_a_target_e2e = sm['modelV2'].action.desiredAcceleration output_should_stop_e2e = sm['modelV2'].action.shouldStop @@ -188,7 +169,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): longitudinalPlan.accels = self.a_desired_trajectory.tolist() longitudinalPlan.jerks = self.j_desired_trajectory.tolist() - longitudinalPlan.hasLead = sm['radarState'].leadOne.status + longitudinalPlan.hasLead = sm['radarState'].leadOne.present longitudinalPlan.longitudinalPlanSource = self.mpc.source longitudinalPlan.fcw = self.fcw diff --git a/selfdrive/controls/plannerd.py b/openpilot/selfdrive/controls/plannerd.py similarity index 85% rename from selfdrive/controls/plannerd.py rename to openpilot/selfdrive/controls/plannerd.py index f7d3370f90..3a05ea174b 100755 --- a/selfdrive/controls/plannerd.py +++ b/openpilot/selfdrive/controls/plannerd.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 -from cereal import car, custom +from openpilot.cereal import custom +from opendbc.car.structs import car from openpilot.common.gps import get_gps_location_service from openpilot.common.params import Params from openpilot.common.realtime import Priority, config_realtime_process from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.controls.lib.ldw import LaneDepartureWarning from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging def main(): @@ -22,13 +23,14 @@ def main(): cloudlog.info("plannerd got CarParamsSP") gps_location_service = get_gps_location_service(params) + ignore_services = ["liveMapDataSP", gps_location_service] ldw = LaneDepartureWarning() longitudinal_planner = LongitudinalPlanner(CP, CP_SP) pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP']) sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState', 'liveMapDataSP', 'carStateSP', gps_location_service], - poll='carState') + poll='carState', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services) while True: sm.update() diff --git a/selfdrive/controls/radard.py b/openpilot/selfdrive/controls/radard.py similarity index 93% rename from selfdrive/controls/radard.py rename to openpilot/selfdrive/controls/radard.py index d51eb6edb0..5db5865e75 100755 --- a/selfdrive/controls/radard.py +++ b/openpilot/selfdrive/controls/radard.py @@ -5,7 +5,8 @@ from collections import deque from typing import Any import capnp -from cereal import messaging, log, car, custom +from openpilot.cereal import messaging, log, custom +from opendbc.car.structs import car from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL, Priority, config_realtime_process @@ -26,7 +27,6 @@ SPEED, ACCEL = 0, 1 # Kalman filter states enum # stationary qualification parameters V_EGO_STATIONARY = 4. # no stationary object flag below this speed -RADAR_TO_CENTER = 2.7 # (deprecated) RADAR is ~ 2.7m ahead from center of car RADAR_TO_CAMERA = 1.52 # RADAR is ~ 1.5m ahead from center of mesh frame @@ -62,13 +62,12 @@ class Track: self.K_K = kalman_params.K self.kf = KF1D([[v_lead], [0.0]], self.K_A, self.K_C, self.K_K) - def update(self, d_rel: float, y_rel: float, v_rel: float, v_lead: float, measured: float): + def update(self, d_rel: float, y_rel: float, v_rel: float, v_lead: float): # relative values, copy self.dRel = d_rel # LONG_DIST self.yRel = y_rel # -LAT_DIST self.vRel = v_rel # REL_SPEED self.vLead = v_lead - self.measured = measured # measured or estimate # computed velocity and accelerations if self.cnt > 0: @@ -94,8 +93,7 @@ class Track: "vLeadK": float(self.vLeadK), "aLeadK": float(self.aLeadK), "aLeadTau": float(self.aLeadTau.x), - "status": True, - "fcw": self.is_potential_fcw(model_prob), + "present": True, "modelProb": model_prob, "radar": True, "radarTrackId": self.identifier, @@ -106,9 +104,6 @@ class Track: # Radar points closer than 0.75, are almost always glitches on toyota radars return abs(self.yRel) < 1.0 and (v_ego < V_EGO_STATIONARY) and (0.75 < self.dRel < 25) - def is_potential_fcw(self, model_prob: float): - return model_prob > .9 - def __str__(self): ret = f"x: {self.dRel:4.1f} y: {self.yRel:4.1f} v: {self.vRel:4.1f} a: {self.aLeadK:4.1f}" return ret @@ -152,9 +147,8 @@ def get_RadarState_from_vision(lead_msg: capnp._DynamicStructReader, v_ego: floa "vLeadK": float(v_ego + lead_v_rel_pred), "aLeadK": float(lead_msg.a[0]), "aLeadTau": 0.3, - "fcw": False, "modelProb": float(lead_prob), - "status": True, + "present": True, "radar": False, "radarTrackId": -1, } @@ -169,7 +163,7 @@ def get_lead(v_ego: float, ready: bool, tracks: dict[int, Track], lead_msg: capn else: track = None - lead_dict = {'status': False} + lead_dict = {'present': False} if track is not None: lead_dict = track.get_RadarState(lead_prob) lead_dict = get_custom_yrel(CP, CP_SP, lead_dict, lead_msg) @@ -182,7 +176,7 @@ def get_lead(v_ego: float, ready: bool, tracks: dict[int, Track], lead_msg: capn closest_track = min(low_speed_tracks, key=lambda c: c.dRel) # Only choose new track if it is actually closer than the previous one - if (not lead_dict['status']) or (closest_track.dRel < lead_dict['dRel']): + if (not lead_dict['present']) or (closest_track.dRel < lead_dict['dRel']): lead_dict = closest_track.get_RadarState() return lead_dict @@ -226,7 +220,7 @@ class RadarD: self.v_ego_hist.append(self.v_ego) self.last_v_ego_frame = sm.recv_frame['carState'] - ar_pts = {pt.trackId: [pt.dRel, pt.yRel, pt.vRel, pt.measured] for pt in rr.points} + ar_pts = {pt.trackId: [pt.dRel, pt.yRel, pt.vRel] for pt in rr.points} # *** remove missing points from meta data *** for ids in list(self.tracks.keys()): @@ -243,14 +237,13 @@ class RadarD: # create the track if it doesn't exist or it's a new track if ids not in self.tracks: self.tracks[ids] = Track(ids, v_lead, self.kalman_params) - self.tracks[ids].update(rpt[0], rpt[1], rpt[2], v_lead, rpt[3]) + self.tracks[ids].update(rpt[0], rpt[1], rpt[2], v_lead) # *** publish radarState *** self.radar_state_valid = sm.all_checks() self.radar_state = log.RadarState.new_message() self.radar_state.mdMonoTime = sm.logMonoTime['modelV2'] self.radar_state.radarErrors = rr.errors - self.radar_state.carStateMonoTime = sm.logMonoTime['carState'] if len(sm['modelV2'].velocity.x): model_v_ego = sm['modelV2'].velocity.x[0] diff --git a/selfdrive/controls/tests/__init__.py b/openpilot/selfdrive/controls/tests/__init__.py similarity index 100% rename from selfdrive/controls/tests/__init__.py rename to openpilot/selfdrive/controls/tests/__init__.py diff --git a/selfdrive/controls/tests/test_following_distance.py b/openpilot/selfdrive/controls/tests/test_following_distance.py similarity index 98% rename from selfdrive/controls/tests/test_following_distance.py rename to openpilot/selfdrive/controls/tests/test_following_distance.py index 1eb88d7206..fcabce0387 100644 --- a/selfdrive/controls/tests/test_following_distance.py +++ b/openpilot/selfdrive/controls/tests/test_following_distance.py @@ -2,7 +2,7 @@ import pytest import itertools from openpilot.common.parameterized import parameterized_class -from cereal import log +from openpilot.cereal import log 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 diff --git a/selfdrive/controls/tests/test_latcontrol.py b/openpilot/selfdrive/controls/tests/test_latcontrol.py similarity index 97% rename from selfdrive/controls/tests/test_latcontrol.py rename to openpilot/selfdrive/controls/tests/test_latcontrol.py index d36ed52192..52e6912d6d 100644 --- a/selfdrive/controls/tests/test_latcontrol.py +++ b/openpilot/selfdrive/controls/tests/test_latcontrol.py @@ -1,6 +1,7 @@ from openpilot.common.parameterized import parameterized -from cereal import car, log +from openpilot.cereal import log +from opendbc.car.structs import car from opendbc.car.car_helpers import interfaces from opendbc.car.honda.values import CAR as HONDA from opendbc.car.toyota.values import CAR as TOYOTA diff --git a/selfdrive/controls/tests/test_latcontrol_torque_buffer.py b/openpilot/selfdrive/controls/tests/test_latcontrol_torque_buffer.py similarity index 96% rename from selfdrive/controls/tests/test_latcontrol_torque_buffer.py rename to openpilot/selfdrive/controls/tests/test_latcontrol_torque_buffer.py index b13576a6cf..dc634f20fd 100644 --- a/selfdrive/controls/tests/test_latcontrol_torque_buffer.py +++ b/openpilot/selfdrive/controls/tests/test_latcontrol_torque_buffer.py @@ -1,6 +1,7 @@ from openpilot.common.parameterized import parameterized -from cereal import car, log +from openpilot.cereal import log +from opendbc.car.structs import car from opendbc.car.car_helpers import interfaces from opendbc.car.toyota.values import CAR as TOYOTA from opendbc.car.vehicle_model import VehicleModel diff --git a/selfdrive/controls/tests/test_leads.py b/openpilot/selfdrive/controls/tests/test_leads.py similarity index 96% rename from selfdrive/controls/tests/test_leads.py rename to openpilot/selfdrive/controls/tests/test_leads.py index 77384fea20..1956bb34ec 100644 --- a/selfdrive/controls/tests/test_leads.py +++ b/openpilot/selfdrive/controls/tests/test_leads.py @@ -1,4 +1,4 @@ -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from opendbc.car.toyota.values import CAR as TOYOTA from openpilot.selfdrive.test.process_replay import replay_process_with_name diff --git a/selfdrive/controls/tests/test_longcontrol.py b/openpilot/selfdrive/controls/tests/test_longcontrol.py similarity index 52% rename from selfdrive/controls/tests/test_longcontrol.py rename to openpilot/selfdrive/controls/tests/test_longcontrol.py index cf0ab24e0b..b2906b4574 100644 --- a/selfdrive/controls/tests/test_longcontrol.py +++ b/openpilot/selfdrive/controls/tests/test_longcontrol.py @@ -1,59 +1,43 @@ -from cereal import car, custom +from openpilot.cereal import custom from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState, long_control_state_trans - - class TestLongControlStateTransition: def test_stay_stopped(self): - CP = car.CarParams.new_message() CP_SP = custom.CarParamsSP.new_message() active = True current_state = LongCtrlState.stopping - next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1, + next_state = long_control_state_trans(CP_SP, active, current_state, should_stop=True, brake_pressed=False, cruise_standstill=False) assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1, + next_state = long_control_state_trans(CP_SP, active, current_state, should_stop=False, brake_pressed=True, cruise_standstill=False) assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1, + next_state = long_control_state_trans(CP_SP, active, current_state, should_stop=False, brake_pressed=False, cruise_standstill=True) assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=1.0, + next_state = long_control_state_trans(CP_SP, active, current_state, should_stop=False, brake_pressed=False, cruise_standstill=False) assert next_state == LongCtrlState.pid active = False - next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=1.0, + next_state = long_control_state_trans(CP_SP, active, current_state, should_stop=False, brake_pressed=False, cruise_standstill=False) assert next_state == LongCtrlState.off def test_engage(): - CP = car.CarParams.new_message() CP_SP = custom.CarParamsSP.new_message() active = True current_state = LongCtrlState.off - next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1, + next_state = long_control_state_trans(CP_SP, active, current_state, should_stop=True, brake_pressed=False, cruise_standstill=False) assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1, + next_state = long_control_state_trans(CP_SP, active, current_state, should_stop=False, brake_pressed=True, cruise_standstill=False) assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1, + next_state = long_control_state_trans(CP_SP, active, current_state, should_stop=False, brake_pressed=False, cruise_standstill=True) assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1, - should_stop=False, brake_pressed=False, cruise_standstill=False) - assert next_state == LongCtrlState.pid - -def test_starting(): - CP = car.CarParams.new_message(startingState=True, vEgoStarting=0.5) - CP_SP = custom.CarParamsSP.new_message() - active = True - current_state = LongCtrlState.starting - next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1, - should_stop=False, brake_pressed=False, cruise_standstill=False) - assert next_state == LongCtrlState.starting - next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=1.0, + next_state = long_control_state_trans(CP_SP, active, current_state, should_stop=False, brake_pressed=False, cruise_standstill=False) assert next_state == LongCtrlState.pid diff --git a/selfdrive/controls/tests/test_torqued_lat_accel_offset.py b/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py similarity index 76% rename from selfdrive/controls/tests/test_torqued_lat_accel_offset.py rename to openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py index 2f95d7c14f..7599eb1e7b 100644 --- a/selfdrive/controls/tests/test_torqued_lat_accel_offset.py +++ b/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py @@ -1,13 +1,12 @@ import numpy as np -from cereal import car, messaging +from openpilot.cereal import messaging +from opendbc.car.structs import car from opendbc.car import ACCELERATION_DUE_TO_GRAVITY from opendbc.car import structs from opendbc.car.lateral import get_friction, FRICTION_THRESHOLD from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.locationd.torqued import TorqueEstimator, MIN_BUCKET_POINTS, POINTS_PER_BUCKET, STEER_BUCKET_BOUNDS -np.random.seed(0) - LA_ERR_STD = 1.0 INPUT_NOISE_STD = 0.08 V_EGO = 30.0 @@ -57,16 +56,17 @@ def simulate_straight_road_msgs(est): for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('livePose', livePose)): est.handle_log(t, which, msg) -def test_estimated_offset(): - steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) - est = get_warmed_up_estimator(steer_torques, lat_accels) - msg = est.get_msg() - # TODO add lataccelfactor and friction check when we have more accurate estimates - assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1 +class TestTorquedLatAccelOffset: + def test_estimated_offset(self): + steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) + est = get_warmed_up_estimator(steer_torques, lat_accels) + msg = est.get_msg() + # TODO add lataccelfactor and friction check when we have more accurate estimates + assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1 -def test_straight_road_roll_bias(): - steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) - est = get_warmed_up_estimator(steer_torques, lat_accels) - simulate_straight_road_msgs(est) - msg = est.get_msg() - assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw) + def test_straight_road_roll_bias(self): + steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) + est = get_warmed_up_estimator(steer_torques, lat_accels) + simulate_straight_road_msgs(est) + msg = est.get_msg() + assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw) diff --git a/selfdrive/locationd/SConscript b/openpilot/selfdrive/locationd/SConscript similarity index 100% rename from selfdrive/locationd/SConscript rename to openpilot/selfdrive/locationd/SConscript diff --git a/selfdrive/debug/__init__.py b/openpilot/selfdrive/locationd/__init__.py similarity index 100% rename from selfdrive/debug/__init__.py rename to openpilot/selfdrive/locationd/__init__.py diff --git a/selfdrive/locationd/calibrationd.py b/openpilot/selfdrive/locationd/calibrationd.py similarity index 98% rename from selfdrive/locationd/calibrationd.py rename to openpilot/selfdrive/locationd/calibrationd.py index 4423f39061..b9616bf632 100755 --- a/selfdrive/locationd/calibrationd.py +++ b/openpilot/selfdrive/locationd/calibrationd.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 ''' This process finds calibration values. More info on what these calibration values -are can be found here https://github.com/commaai/openpilot/tree/master/common/transformations +are can be found here https://github.com/commaai/openpilot/tree/master/openpilot/common/transformations While the roll calibration is a real value that can be estimated, here we assume it's zero, and the image input into the neural network is not corrected for roll. ''' @@ -11,9 +11,10 @@ import capnp import numpy as np from typing import NoReturn -from cereal import log, car -import cereal.messaging as messaging -from openpilot.system.hardware import HARDWARE +from openpilot.cereal import log +from opendbc.car.structs import car +import openpilot.cereal.messaging as messaging +from openpilot.common.hardware import HARDWARE from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process diff --git a/selfdrive/locationd/helpers.py b/openpilot/selfdrive/locationd/helpers.py similarity index 95% rename from selfdrive/locationd/helpers.py rename to openpilot/selfdrive/locationd/helpers.py index 73c4d8bf35..5e3fa16027 100644 --- a/selfdrive/locationd/helpers.py +++ b/openpilot/selfdrive/locationd/helpers.py @@ -1,8 +1,9 @@ import numpy as np +from collections.abc import Sequence from typing import Any from functools import cache -from cereal import log +from openpilot.cereal import log from openpilot.common.transformations.orientation import rot_from_euler, euler_from_rot @@ -68,7 +69,8 @@ class NPQueue: class PointBuckets: - def __init__(self, x_bounds: list[tuple[float, float]], min_points: list[float], min_points_total: int, points_per_bucket: int, rowsize: int) -> None: + def __init__(self, x_bounds: list[tuple[float, float]], min_points: Sequence[float], min_points_total: int, points_per_bucket: int, rowsize: int) -> None: + self._rng = np.random.default_rng() self.x_bounds = x_bounds self.buckets = {bounds: NPQueue(maxlen=points_per_bucket, rowsize=rowsize) for bounds in x_bounds} self.buckets_min_points = dict(zip(x_bounds, min_points, strict=True)) @@ -98,9 +100,9 @@ class PointBuckets: points = np.vstack([x.arr for x in self.buckets.values()]) if num_points is None: return points - return points[np.random.choice(np.arange(len(points)), min(len(points), num_points), replace=False)] + return points[self._rng.choice(np.arange(len(points)), min(len(points), num_points), replace=False)] - def load_points(self, points: list[list[float]]) -> None: + def load_points(self, points: Sequence[Sequence[float]]) -> None: for point in points: self.add_point(*point) diff --git a/selfdrive/locationd/lagd.py b/openpilot/selfdrive/locationd/lagd.py similarity index 96% rename from selfdrive/locationd/lagd.py rename to openpilot/selfdrive/locationd/lagd.py index 2980e61e5e..da2c53fd11 100755 --- a/selfdrive/locationd/lagd.py +++ b/openpilot/selfdrive/locationd/lagd.py @@ -5,9 +5,11 @@ import capnp from collections import deque from functools import partial -import cereal.messaging as messaging -from cereal import car, log -from cereal.services import SERVICE_LIST +import openpilot.cereal.messaging as messaging +from openpilot.cereal import log +from opendbc.car.structs import car +from openpilot.cereal.services import SERVICE_LIST +from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process from openpilot.common.swaglog import cloudlog @@ -20,11 +22,11 @@ BLOCK_NUM_NEEDED = 5 MOVING_WINDOW_SEC = 60.0 MIN_OKAY_WINDOW_SEC = 25.0 MIN_RECOVERY_BUFFER_SEC = 2.0 -MIN_VEGO = 15.0 +MIN_VEGO = 50.0 * CV.MPH_TO_MS MIN_ABS_YAW_RATE = 0.0 MAX_YAW_RATE_SANITY_CHECK = 1.0 MIN_NCC = 0.95 -MAX_LAG = 1.0 +MAX_LAG = 0.65 MIN_LAG = 0.15 MAX_LAG_STD = 0.1 MAX_LAT_ACCEL = 2.0 @@ -36,6 +38,8 @@ LAG_CANDIDATE_CORR_THRESHOLD = 0.9 SMOOTH_K = 5 SMOOTH_SIGMA = 1.0 +VERSION = 1 # bump this to invalidate old parameter caches + def masked_symmetric_moving_average(x: np.ndarray, mask: np.ndarray, k: int, sigma: float) -> np.ndarray: assert k >= 1 and k % 2 == 1, "k must be positive and odd" @@ -248,6 +252,7 @@ class LateralLagEstimator: (self.min_valid_block_count * self.block_size), 100) if debug: liveDelay.points = self.block_avg.values.flatten().tolist() + liveDelay.version = VERSION return msg @@ -368,9 +373,10 @@ def retrieve_initial_lag(params: Params, CP: car.CarParams): if last_CP.carFingerprint != CP.carFingerprint: raise Exception("Car model mismatch") - lag, valid_blocks, status = ld.lateralDelayEstimate, ld.validBlocks, ld.status + lag, valid_blocks, status, version = ld.lateralDelayEstimate, ld.validBlocks, ld.status, ld.version assert valid_blocks <= BLOCK_NUM, "Invalid number of valid blocks" assert status != log.LiveDelayData.Status.invalid, "Lag estimate is invalid" + assert version == VERSION, f"Lag estimate is from a different version (got {version}, expected {VERSION})" return lag, valid_blocks except Exception as e: cloudlog.error(f"Failed to retrieve initial lag: {e}") diff --git a/selfdrive/locationd/locationd.py b/openpilot/selfdrive/locationd/locationd.py similarity index 97% rename from selfdrive/locationd/locationd.py rename to openpilot/selfdrive/locationd/locationd.py index 57aecb22e7..eb3c42fce2 100755 --- a/selfdrive/locationd/locationd.py +++ b/openpilot/selfdrive/locationd/locationd.py @@ -6,8 +6,8 @@ import numpy as np from enum import Enum from collections import defaultdict -from cereal import log, messaging -from cereal.services import SERVICE_LIST +from openpilot.cereal import log, messaging +from openpilot.cereal.services import SERVICE_LIST from openpilot.common.transformations.orientation import rot_from_euler from openpilot.common.realtime import config_realtime_process from openpilot.common.params import Params @@ -66,7 +66,7 @@ class LocationEstimator: self.observations = {kind: np.zeros(3, dtype=np.float32) for kind in obs_kinds} self.observation_errors = {kind: np.zeros(3, dtype=np.float32) for kind in obs_kinds} - def reset(self, t: float, x_initial: np.ndarray = PoseKalman.initial_x, P_initial: np.ndarray = PoseKalman.initial_P): + def reset(self, t: float | None, x_initial: np.ndarray = PoseKalman.initial_x, P_initial: np.ndarray = PoseKalman.initial_P): self.kf.init_state(x_initial, covs=P_initial, filter_time=t) def _validate_sensor_source(self, source: log.SensorEventData.SensorSource): @@ -206,9 +206,11 @@ class LocationEstimator: self._finite_check(t, new_x, new_P) return HandleLogResult.SUCCESS - def get_msg(self, sensors_valid: bool, inputs_valid: bool, filter_valid: bool): + def get_msg(self, sensors_valid: bool, inputs_valid: bool, filter_initialized: bool): state, cov = self.kf.x, self.kf.P std = np.sqrt(np.diag(cov)) + filter_time_valid = bool(np.isfinite(self.kf.t)) + filter_valid = filter_initialized and filter_time_valid orientation_ned, orientation_ned_std = state[States.NED_ORIENTATION], std[States.NED_ORIENTATION] velocity_device, velocity_device_std = state[States.DEVICE_VELOCITY], std[States.DEVICE_VELOCITY] diff --git a/selfdrive/locationd/models/.gitignore b/openpilot/selfdrive/locationd/models/.gitignore similarity index 100% rename from selfdrive/locationd/models/.gitignore rename to openpilot/selfdrive/locationd/models/.gitignore diff --git a/selfdrive/locationd/__init__.py b/openpilot/selfdrive/locationd/models/__init__.py similarity index 100% rename from selfdrive/locationd/__init__.py rename to openpilot/selfdrive/locationd/models/__init__.py diff --git a/selfdrive/locationd/models/car_kf.py b/openpilot/selfdrive/locationd/models/car_kf.py similarity index 100% rename from selfdrive/locationd/models/car_kf.py rename to openpilot/selfdrive/locationd/models/car_kf.py diff --git a/selfdrive/locationd/models/constants.py b/openpilot/selfdrive/locationd/models/constants.py similarity index 100% rename from selfdrive/locationd/models/constants.py rename to openpilot/selfdrive/locationd/models/constants.py diff --git a/selfdrive/locationd/models/pose_kf.py b/openpilot/selfdrive/locationd/models/pose_kf.py similarity index 100% rename from selfdrive/locationd/models/pose_kf.py rename to openpilot/selfdrive/locationd/models/pose_kf.py diff --git a/selfdrive/locationd/paramsd.py b/openpilot/selfdrive/locationd/paramsd.py similarity index 91% rename from selfdrive/locationd/paramsd.py rename to openpilot/selfdrive/locationd/paramsd.py index 02d09c506e..c1088a8180 100755 --- a/selfdrive/locationd/paramsd.py +++ b/openpilot/selfdrive/locationd/paramsd.py @@ -3,8 +3,9 @@ import os import numpy as np import capnp -import cereal.messaging as messaging -from cereal import car, log +import openpilot.cereal.messaging as messaging +from openpilot.cereal import log +from opendbc.car.structs import car from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process, DT_MDL from openpilot.selfdrive.locationd.models.car_kf import CarKalman, ObservationKind, States @@ -199,25 +200,6 @@ def check_valid_with_hysteresis(current_valid: bool, val: float, threshold: floa return current_valid -# TODO: Remove this function after few releases (added in 0.9.9) -def migrate_cached_vehicle_params_if_needed(params: Params): - last_parameters_data_old = params.get("LiveParameters") - last_parameters_data = params.get("LiveParametersV2") - if last_parameters_data_old is None or last_parameters_data is not None: - return - - try: - last_parameters_msg = messaging.new_message('liveParameters') - last_parameters_msg.liveParameters.valid = True - last_parameters_msg.liveParameters.steerRatio = last_parameters_data_old['steerRatio'] - last_parameters_msg.liveParameters.stiffnessFactor = last_parameters_data_old['stiffnessFactor'] - last_parameters_msg.liveParameters.angleOffsetAverageDeg = last_parameters_data_old['angleOffsetAverageDeg'] - params.put("LiveParametersV2", last_parameters_msg.to_bytes(), block=True) - except Exception as e: - cloudlog.error(f"Failed to perform parameter migration: {e}") - params.remove("LiveParameters") - - def retrieve_initial_vehicle_params(params: Params, CP: car.CarParams, replay: bool, debug: bool): last_parameters_data = params.get("LiveParametersV2") last_carparams_data = params.get("CarParamsPrevRoute") @@ -272,8 +254,6 @@ def main(): params = Params() CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) - migrate_cached_vehicle_params_if_needed(params) - steer_ratio, stiffness_factor, angle_offset_deg, pInitial = retrieve_initial_vehicle_params(params, CP, REPLAY, DEBUG) learner = VehicleParamsLearner(CP, steer_ratio, stiffness_factor, np.radians(angle_offset_deg), pInitial) diff --git a/selfdrive/locationd/models/__init__.py b/openpilot/selfdrive/locationd/test/__init__.py similarity index 100% rename from selfdrive/locationd/models/__init__.py rename to openpilot/selfdrive/locationd/test/__init__.py diff --git a/selfdrive/locationd/test/test_calibrationd.py b/openpilot/selfdrive/locationd/test/test_calibrationd.py similarity index 98% rename from selfdrive/locationd/test/test_calibrationd.py rename to openpilot/selfdrive/locationd/test/test_calibrationd.py index c90ce506ab..f862b369fe 100644 --- a/selfdrive/locationd/test/test_calibrationd.py +++ b/openpilot/selfdrive/locationd/test/test_calibrationd.py @@ -2,8 +2,8 @@ import random import numpy as np -import cereal.messaging as messaging -from cereal import log +import openpilot.cereal.messaging as messaging +from openpilot.cereal import log from openpilot.common.params import Params from openpilot.selfdrive.locationd.calibrationd import Calibrator, INPUTS_NEEDED, INPUTS_WANTED, BLOCK_SIZE, MIN_SPEED_FILTER, \ MAX_YAW_RATE_FILTER, SMOOTH_CYCLES, HEIGHT_INIT, MAX_ALLOWED_PITCH_SPREAD, MAX_ALLOWED_YAW_SPREAD diff --git a/selfdrive/locationd/test/test_lagd.py b/openpilot/selfdrive/locationd/test/test_lagd.py similarity index 75% rename from selfdrive/locationd/test/test_lagd.py rename to openpilot/selfdrive/locationd/test/test_lagd.py index 746f7ad478..efaba6ead9 100644 --- a/selfdrive/locationd/test/test_lagd.py +++ b/openpilot/selfdrive/locationd/test/test_lagd.py @@ -3,20 +3,22 @@ import numpy as np import time import pytest -from cereal import messaging, log, car +from openpilot.cereal import messaging, log +from opendbc.car.structs import car from openpilot.selfdrive.locationd.lagd import LateralLagEstimator, retrieve_initial_lag, masked_normalized_cross_correlation, \ - BLOCK_NUM_NEEDED, BLOCK_SIZE, MIN_OKAY_WINDOW_SEC + BLOCK_NUM_NEEDED, BLOCK_SIZE, MIN_OKAY_WINDOW_SEC, VERSION, MIN_LAG, MAX_LAG from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_carParams from openpilot.selfdrive.locationd.test.test_locationd_scenarios import TEST_ROUTE from openpilot.common.params import Params from openpilot.tools.lib.logreader import LogReader -from openpilot.system.hardware import PC +from openpilot.common.hardware import PC MAX_ERR_FRAMES = 1 DT = 0.05 +LAGD_MIN_LAG_FRAMES, LAGD_MAX_LAG_FRAMES = int(round(MIN_LAG / DT)), int(round(MAX_LAG / DT)) -def process_messages(estimator, lag_frames, n_frames, vego=20.0, rejection_threshold=0.0): +def process_messages(estimator, lag_frames, n_frames, vego=25.0, rejection_threshold=0.0): for i in range(n_frames): t = i * estimator.dt desired_la = np.cos(10 * t) * 0.3 @@ -53,6 +55,7 @@ class TestLagd: msg = messaging.new_message('liveDelay') msg.liveDelay.lateralDelayEstimate = random.random() msg.liveDelay.validBlocks = random.randint(1, 10) + msg.liveDelay.version = VERSION params.put("LiveDelay", msg.to_bytes(), block=True) params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True) @@ -63,7 +66,22 @@ class TestLagd: assert lag == msg.liveDelay.lateralDelayEstimate assert valid_blocks == msg.liveDelay.validBlocks + def test_read_invalid_saved_params(self, subtests): + params = Params() + + lr = migrate(LogReader(TEST_ROUTE), [migrate_carParams]) + CP = next(m for m in lr if m.which() == "carParams").carParams + + for msg_dict in [{'version': 0}, {'status': 'invalid'}, {'validBlocks': 100}]: + with subtests.test(msg=f"liveDelay={msg_dict}"): + msg = messaging.new_message('liveDelay') + msg.liveDelay = msg_dict + params.put("LiveDelay", msg.to_bytes(), block=True) + params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True) + assert retrieve_initial_lag(params, CP) is None + def test_ncc(self): + rng = np.random.default_rng() lag_frames = random.randint(1, 19) desired_sig = np.sin(np.arange(0.0, 10.0, 0.1)) @@ -74,20 +92,20 @@ class TestLagd: assert np.argmax(corr) == lag_frames # add some noise - desired_sig += np.random.normal(0, 0.05, len(desired_sig)) - actual_sig += np.random.normal(0, 0.05, len(actual_sig)) + desired_sig += rng.normal(0, 0.05, len(desired_sig)) + actual_sig += rng.normal(0, 0.05, len(actual_sig)) corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20] assert np.argmax(corr) in range(lag_frames - MAX_ERR_FRAMES, lag_frames + MAX_ERR_FRAMES + 1) # mask out 40% of the values, and make them noise - mask = np.random.choice([True, False], size=len(desired_sig), p=[0.6, 0.4]) - desired_sig[~mask] = np.random.normal(0, 1, size=np.sum(~mask)) - actual_sig[~mask] = np.random.normal(0, 1, size=np.sum(~mask)) + mask = rng.choice([True, False], size=len(desired_sig), p=[0.6, 0.4]) + desired_sig[~mask] = rng.normal(0, 1, size=np.sum(~mask)) + actual_sig[~mask] = rng.normal(0, 1, size=np.sum(~mask)) corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20] assert np.argmax(corr) in range(lag_frames - MAX_ERR_FRAMES, lag_frames + MAX_ERR_FRAMES + 1) def test_empty_estimator(self): - mocked_CP = car.CarParams(steerActuatorDelay=0.8) + mocked_CP = car.CarParams(steerActuatorDelay=0.5) estimator = LateralLagEstimator(mocked_CP, DT) msg = estimator.get_msg(True) assert msg.liveDelay.status == 'unestimated' @@ -97,9 +115,9 @@ class TestLagd: assert msg.liveDelay.calPerc == 0 def test_estimator_basics(self, subtests): - for lag_frames in range(3, 10): + for lag_frames in range(LAGD_MIN_LAG_FRAMES, LAGD_MAX_LAG_FRAMES - 1): with subtests.test(msg=f"lag_frames={lag_frames}"): - mocked_CP = car.CarParams(steerActuatorDelay=0.8) + mocked_CP = car.CarParams(steerActuatorDelay=0.5) estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0) process_messages(estimator, lag_frames, int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_NUM_NEEDED * BLOCK_SIZE) msg = estimator.get_msg(True) @@ -111,7 +129,7 @@ class TestLagd: assert msg.liveDelay.calPerc == 100 def test_estimator_masking(self): - mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(3, 19) + mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.5), random.randint(LAGD_MIN_LAG_FRAMES, LAGD_MAX_LAG_FRAMES - 1) estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, min_valid_block_count=1) process_messages(estimator, lag_frames, (int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_SIZE) * 2, rejection_threshold=0.4) msg = estimator.get_msg(True) @@ -121,7 +139,7 @@ class TestLagd: @pytest.mark.skipif(PC, reason="only on device") def test_estimator_performance(self): - mocked_CP = car.CarParams(steerActuatorDelay=0.8) + mocked_CP = car.CarParams(steerActuatorDelay=0.5) estimator = LateralLagEstimator(mocked_CP, DT) ds = [] diff --git a/selfdrive/locationd/test/test_locationd_scenarios.py b/openpilot/selfdrive/locationd/test/test_locationd_scenarios.py similarity index 98% rename from selfdrive/locationd/test/test_locationd_scenarios.py rename to openpilot/selfdrive/locationd/test/test_locationd_scenarios.py index 69f2ca2821..04d2d6b55b 100644 --- a/selfdrive/locationd/test/test_locationd_scenarios.py +++ b/openpilot/selfdrive/locationd/test/test_locationd_scenarios.py @@ -37,9 +37,9 @@ def get_select_fields_data(logs): def sig_smooth(signal): return masked_symmetric_moving_average(signal, np.ones_like(signal), 5, 1.0) def get_nested_keys(msg, keys): - val = None + val = msg for key in keys: - val = getattr(msg if val is None else val, key) if isinstance(key, str) else val[key] + val = getattr(val, key) if isinstance(key, str) else val[key] return val lp = [x.livePose for x in logs if x.which() == 'livePose'] data = defaultdict(list) diff --git a/selfdrive/locationd/test/test_paramsd.py b/openpilot/selfdrive/locationd/test/test_paramsd.py similarity index 53% rename from selfdrive/locationd/test/test_paramsd.py rename to openpilot/selfdrive/locationd/test/test_paramsd.py index 1a4d348e94..74135cb875 100644 --- a/selfdrive/locationd/test/test_paramsd.py +++ b/openpilot/selfdrive/locationd/test/test_paramsd.py @@ -1,8 +1,8 @@ import random import numpy as np -from cereal import messaging -from openpilot.selfdrive.locationd.paramsd import retrieve_initial_vehicle_params, migrate_cached_vehicle_params_if_needed +from openpilot.cereal import messaging +from openpilot.selfdrive.locationd.paramsd import retrieve_initial_vehicle_params from openpilot.selfdrive.locationd.models.car_kf import CarKalman from openpilot.selfdrive.locationd.test.test_locationd_scenarios import TEST_ROUTE from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_carParams @@ -30,38 +30,9 @@ class TestParamsd: params.put("LiveParametersV2", msg.to_bytes(), block=True) params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True) - migrate_cached_vehicle_params_if_needed(params) # this is not tested here but should not mess anything up or throw an error sr, sf, offset, p_init = retrieve_initial_vehicle_params(params, CP, replay=True, debug=True) np.testing.assert_allclose(sr, msg.liveParameters.steerRatio) np.testing.assert_allclose(sf, msg.liveParameters.stiffnessFactor) np.testing.assert_allclose(offset, msg.liveParameters.angleOffsetAverageDeg) np.testing.assert_equal(p_init.shape, CarKalman.P_initial.shape) np.testing.assert_allclose(np.diagonal(p_init), msg.liveParameters.debugFilterState.std) - - # TODO Remove this test after the support for old format is removed - def test_read_saved_params_old_format(self): - params = Params() - - lr = migrate(LogReader(TEST_ROUTE), [migrate_carParams]) - CP = next(m for m in lr if m.which() == "carParams").carParams - - msg = get_random_live_parameters(CP) - params.put("LiveParameters", msg.liveParameters.to_dict(), block=True) - params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True) - params.remove("LiveParametersV2") - - migrate_cached_vehicle_params_if_needed(params) - sr, sf, offset, _ = retrieve_initial_vehicle_params(params, CP, replay=True, debug=True) - np.testing.assert_allclose(sr, msg.liveParameters.steerRatio) - np.testing.assert_allclose(sf, msg.liveParameters.stiffnessFactor) - np.testing.assert_allclose(offset, msg.liveParameters.angleOffsetAverageDeg) - assert params.get("LiveParametersV2") is not None - - def test_read_saved_params_corrupted_old_format(self): - params = Params() - params.put("LiveParameters", {}, block=True) - params.remove("LiveParametersV2") - - migrate_cached_vehicle_params_if_needed(params) - assert params.get("LiveParameters") is None - assert params.get("LiveParametersV2") is None diff --git a/openpilot/selfdrive/locationd/test/test_torqued.py b/openpilot/selfdrive/locationd/test/test_torqued.py new file mode 100644 index 0000000000..4d337178a0 --- /dev/null +++ b/openpilot/selfdrive/locationd/test/test_torqued.py @@ -0,0 +1,26 @@ +from opendbc.car.structs import car +from openpilot.selfdrive.locationd.torqued import TorqueEstimator + + +class TestTorqued: + def test_cal_percent(self): + est = TorqueEstimator(car.CarParams()) + msg = est.get_msg() + assert msg.liveTorqueParameters.calPerc == 0 + + for (low, high), min_pts in zip(est.filtered_points.buckets.keys(), + est.filtered_points.buckets_min_points.values(), strict=True): + for _ in range(int(min_pts)): + est.filtered_points.add_point((low + high) / 2.0, 0.0) + + # enough bucket points, but not enough total points + msg = est.get_msg() + assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2 + + # add enough points to bucket with most capacity + key = list(est.filtered_points.buckets)[0] + for _ in range(est.min_points_total - len(est.filtered_points)): + est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0) + + msg = est.get_msg() + assert msg.liveTorqueParameters.calPerc == 100 diff --git a/selfdrive/locationd/torqued.py b/openpilot/selfdrive/locationd/torqued.py similarity index 96% rename from selfdrive/locationd/torqued.py rename to openpilot/selfdrive/locationd/torqued.py index a0f16f303a..1d4aa8bc35 100755 --- a/selfdrive/locationd/torqued.py +++ b/openpilot/selfdrive/locationd/torqued.py @@ -3,8 +3,9 @@ import os import numpy as np from collections import deque, defaultdict -import cereal.messaging as messaging -from cereal import car, log +import openpilot.cereal.messaging as messaging +from openpilot.cereal import log +from opendbc.car.structs import car from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process, DT_MDL @@ -61,14 +62,14 @@ class TorqueEstimator(ParameterEstimator, TorqueEstimatorExt): self.lag = 0.0 self.track_all_points = track_all_points # for offline analysis, without max lateral accel or max steer torque filters if decimated: - self.min_bucket_points = MIN_BUCKET_POINTS / 10 + self.min_bucket_points: list[float] = (MIN_BUCKET_POINTS / 10).tolist() self.min_points_total = MIN_POINTS_TOTAL_QLOG self.fit_points = FIT_POINTS_TOTAL_QLOG self.factor_sanity = FACTOR_SANITY_QLOG self.friction_sanity = FRICTION_SANITY_QLOG else: - self.min_bucket_points = MIN_BUCKET_POINTS + self.min_bucket_points = MIN_BUCKET_POINTS.tolist() self.min_points_total = MIN_POINTS_TOTAL self.fit_points = FIT_POINTS_TOTAL self.factor_sanity = FACTOR_SANITY @@ -119,9 +120,10 @@ class TorqueEstimator(ParameterEstimator, TorqueEstimatorExt): 'latAccelOffset': cache_ltp.latAccelOffsetFiltered, 'frictionCoefficient': cache_ltp.frictionCoefficientFiltered } - initial_params['points'] = cache_ltp.points + cached_points: list[list[float]] = [list(point) for point in cache_ltp.points] + initial_params['points'] = cached_points self.decay = cache_ltp.decay - self.filtered_points.load_points(initial_params['points']) + self.filtered_points.load_points(cached_points) cloudlog.info("restored torque params from cache") except Exception: cloudlog.exception("failed to restore cached torque params") diff --git a/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript similarity index 86% rename from selfdrive/modeld/SConscript rename to openpilot/selfdrive/modeld/SConscript index 80a7df4515..5f6281556f 100644 --- a/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -16,7 +16,7 @@ CAMERA_CONFIGS = [ ] Import('env', 'arch') -chunker_file = File("#common/file_chunker.py") +chunker_file = File("#openpilot/common/file_chunker.py") lenv = env.Clone() tinygrad_root = env.Dir("#").abspath @@ -45,11 +45,11 @@ else: tg_flags = f'DEV=CPU' if arch == 'Darwin' else 'DEV=CPU:LLVM' tg_devices = { # which device to put jit inputs to at runtime - 'selfdrive.modeld.modeld': { + 'openpilot.selfdrive.modeld.modeld': { 'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend}, 'usbgpu': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'} }, - 'selfdrive.modeld.dmonitoringmodeld': { + 'openpilot.selfdrive.modeld.dmonitoringmodeld': { 'default': {'DEV': tg_backend} }, } @@ -74,27 +74,26 @@ tg_devices_node = lenv.Command( # tinygrad calls brew which needs a $HOME in the env mac_brew_string = f'HOME={os.path.expanduser("~")}' if arch == 'Darwin' else '' -modeld_dir = Dir("#selfdrive/modeld").abspath +modeld_dir = Dir("#openpilot/selfdrive/modeld").abspath compile_modeld_script = [ File(f"{modeld_dir}/compile_modeld.py"), File(f"{modeld_dir}/get_model_metadata.py"), - File("#system/camerad/cameras/nv12_info.py"), - File("#system/hardware/hw.py"), + File("#openpilot/system/camerad/cameras/nv12_info.py"), + File("#openpilot/common/hardware/hw.py"), ] model_w, model_h = MEDMODEL_INPUT_SIZE frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ for usbgpu in [False, True] if USBGPU else [False]: target_pkl_path = File(modeld_pkl_path(usbgpu)).abspath - file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('', tg_flags) - driving_onnx_deps = [p for m in [f'{file_prefix}driving_vision', f'{file_prefix}driving_on_policy'] - for p in get_existing_chunks(File(f"models/{m}.onnx").abspath)] + # BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a USB GPU + file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags) + driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath) camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS) cmd = (f'{cmd_flags} {mac_brew_string} python3 {modeld_dir}/compile_modeld.py ' f'--model-size {model_w}x{model_h} ' f'--camera-resolutions {camera_res_args} ' - f'--vision-onnx {File(f"models/{file_prefix}driving_vision.onnx").abspath} ' - f'--on-policy-onnx {File(f"models/{file_prefix}driving_on_policy.onnx").abspath} ' + f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} ' f'--output {target_pkl_path} --frame-skip {frame_skip}') onnx_sizes_sum = sum(os.path.getsize(f) for f in driving_onnx_deps) chunk_targets = get_chunk_targets(target_pkl_path, estimate_pickle_max_size(onnx_sizes_sum)) @@ -110,8 +109,8 @@ for usbgpu in [False, True] if USBGPU else [False]: # get model metadata fn = File(f"models/dmonitoring_model").abspath -script_files = [File(Dir("#selfdrive/modeld").File("get_model_metadata.py").abspath)] -cmd = f'{tg_flags} {mac_brew_string} python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx' +script_files = [File(Dir("#openpilot/selfdrive/modeld").File("get_model_metadata.py").abspath)] +cmd = f'{tg_flags} {mac_brew_string} python3 {Dir("#openpilot/selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx' lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_files + [tg_devices_node], cmd) dm_w, dm_h = DM_INPUT_SIZE diff --git a/selfdrive/locationd/test/__init__.py b/openpilot/selfdrive/modeld/__init__.py similarity index 100% rename from selfdrive/locationd/test/__init__.py rename to openpilot/selfdrive/modeld/__init__.py diff --git a/selfdrive/modeld/compile_dm_warp.py b/openpilot/selfdrive/modeld/compile_dm_warp.py similarity index 100% rename from selfdrive/modeld/compile_dm_warp.py rename to openpilot/selfdrive/modeld/compile_dm_warp.py diff --git a/selfdrive/modeld/compile_modeld.py b/openpilot/selfdrive/modeld/compile_modeld.py similarity index 61% rename from selfdrive/modeld/compile_modeld.py rename to openpilot/selfdrive/modeld/compile_modeld.py index 2f91076ab7..ebc3f21a13 100755 --- a/selfdrive/modeld/compile_modeld.py +++ b/openpilot/selfdrive/modeld/compile_modeld.py @@ -1,14 +1,20 @@ #!/usr/bin/env python3 import argparse import atexit +import math import os import pickle +import tempfile import time +import shutil from functools import partial -from collections import namedtuple, defaultdict +from collections import namedtuple import numpy as np +from openpilot.selfdrive.modeld.helpers import dump_oob, load_oob +from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link + def _patch_tinygrad_fetch_fw(): import hashlib import pathlib @@ -25,6 +31,23 @@ def _patch_tinygrad_fetch_fw(): helpers.fetch_fw = fetch_fw _patch_tinygrad_fetch_fw() +def _patch_tinygrad_buffer_reduce(): + from tinygrad.device import Buffer + def __reduce_ex__(self, protocol): + buf = None + if self._base is not None: + return self.__class__, (self.device, self.size, self.dtype, None, None, None, 0, self.base, self.offset, self.is_allocated()) + if self.device == "NPY": + return self.__class__, (self.device, self.size, self.dtype, self._buf, self.options, None, self.uop_refcount) + if self.is_allocated(): + buf = bytearray(self.nbytes) + self.copyout(memoryview(buf)) + if protocol >= 5: + buf = pickle.PickleBuffer(buf) + return self.__class__, (self.device, self.size, self.dtype, None, self.options, buf, self.uop_refcount) + Buffer.__reduce_ex__ = __reduce_ex__ +_patch_tinygrad_buffer_reduce() + from tinygrad.tensor import Tensor from tinygrad.helpers import Context from tinygrad.device import Device @@ -32,8 +55,8 @@ from tinygrad.engine.jit import TinyJit NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size']) -WARP_INPUTS = ['img_q', 'big_img_q', 'tfm', 'big_tfm'] -POLICY_INPUTS = ['feat_q', 'desire_q', 'desire', 'traffic_convention', 'action_t'] +WARP_INPUTS = ['tfm', 'big_tfm'] +POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs'] UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32) UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX) @@ -49,8 +72,8 @@ def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, w_dst, h_dst = dst_shape h_src, w_src = src_shape - x = Tensor.arange(w_dst, device=WARP_DEV).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1) - y = Tensor.arange(h_dst, device=WARP_DEV).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1) + x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1) + y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1) # inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather) src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2] @@ -113,34 +136,51 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h): return frame_prepare_tinygrad -def make_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device): +def make_warp_input_queues(vision_input_shapes, frame_skip, device): img = vision_input_shapes['img'] # (1, 12, 128, 256) n_frames = img[1] // 6 img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3]) - fb = policy_input_shapes['features_buffer'] # (1, 25, 512) - dp = policy_input_shapes['desire_pulse'] # (1, 25, 8) - tc = policy_input_shapes['traffic_convention'] # (1, 2) - #TODO action_t is hardcoded to match tc for future compatibility - at = tc - npy = { - 'desire': np.zeros(dp[2], dtype=np.float32), - 'traffic_convention': np.zeros(tc, dtype=np.float32), 'tfm': np.zeros((3, 3), dtype=np.float32), 'big_tfm': np.zeros((3, 3), dtype=np.float32), - 'action_t': np.zeros(at, dtype=np.float32), } input_queues = { 'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), 'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), - 'feat_q': Tensor(np.zeros((frame_skip * (fb[1] - 1) + 1, fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(), - 'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(), **{k: Tensor(v, device='NPY').realize() for k, v in npy.items()}, } return input_queues, npy +def get_policy_npy_shapes(input_shapes): + dp = input_shapes['desire_pulse'] # (1, 25, 8) + tc = input_shapes['traffic_convention'] # (1, 2) + at = input_shapes['action_t'] # (1, 2) + fb = input_shapes['features_buffer'] # (1, 24, 512) + # TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now + shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])} + return shapes, [math.prod(s) for s in shapes.values()] + + +def make_input_queues(input_shapes, frame_skip, device): + input_queues, npy = make_warp_input_queues(input_shapes, frame_skip, device) + + fb = input_shapes['features_buffer'] # (1, 24, 512), past features only; the model appends the current frame's feature + dp = input_shapes['desire_pulse'] # (1, 25, 8) + + shapes, sizes = get_policy_npy_shapes(input_shapes) + packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32) + # views into the packed inputs, to be refilled at runtime + npy.update({k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}) + input_queues.update({ + 'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(), + 'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(), + 'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(), + }) + return input_queues, npy + + def shift_and_sample(buf, new_val, sample_fn): buf.assign(buf[1:].cat(new_val, dim=0).contiguous()) return sample_fn(buf) @@ -156,57 +196,54 @@ def sample_desire(buf, frame_skip): def make_warp(nv12, model_w, model_h, frame_skip): frame_prepare = make_frame_prepare(nv12, model_w, model_h) - sample_skip_fn = partial(sample_skip, frame_skip=frame_skip) - def warp_enqueue(img_q, big_img_q, tfm, big_tfm, frame, big_frame): + def warp(tfm, big_tfm, frame, big_frame): tfm = tfm.to(WARP_DEV) big_tfm = big_tfm.to(WARP_DEV) Tensor.realize(tfm, big_tfm) - warped_frame = frame_prepare(frame, tfm).unsqueeze(0).to(Device.DEFAULT) - warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0).to(Device.DEFAULT) - img = shift_and_sample(img_q, warped_frame, sample_skip_fn) - big_img = shift_and_sample(big_img_q, warped_big_frame, sample_skip_fn) - return img, big_img - return warp_enqueue + warped_frame = frame_prepare(frame, tfm).unsqueeze(0) + warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0) + return Tensor.cat(warped_frame, warped_big_frame) + + return warp -def make_run_policy(vision_runner, on_policy_runner, vision_features_slice, frame_skip): +def make_run_policy(model_runner, model_metadata, frame_skip): sample_desire_fn = partial(sample_desire, frame_skip=frame_skip) sample_skip_fn = partial(sample_skip, frame_skip=frame_skip) + npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes']) - def run_policy(img, big_img, feat_q, desire_q, desire, traffic_convention, action_t): - desire = desire.to(Device.DEFAULT) - traffic_convention = traffic_convention.to(Device.DEFAULT) - action_t = action_t.to(Device.DEFAULT) - Tensor.realize(desire, traffic_convention, action_t) + def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs): + packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT) + warped = warped.to(Device.DEFAULT) + Tensor.realize(packed_npy_inputs, warped) + + img = shift_and_sample(img_q, warped[0:1], sample_skip_fn) + big_img = shift_and_sample(big_img_q, warped[1:2], sample_skip_fn) + + desire, traffic_convention, action_t, prev_feat = (t.reshape(s) for t, s in zip(packed_npy_inputs.split(npy_sizes), npy_shapes.values(), strict=True)) desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn) - vision_out = next(iter(vision_runner({'img': img, 'big_img': big_img}).values())).cast('float32') - - new_feat = vision_out[:, vision_features_slice].reshape(1, -1).unsqueeze(0) - feat_buf = shift_and_sample(feat_q, new_feat, sample_skip_fn) + feat_buf = shift_and_sample(feat_q, prev_feat.reshape(1, 1, -1), sample_skip_fn) inputs = { + 'img': img, + 'big_img': big_img, 'features_buffer': feat_buf, 'desire_pulse': desire_buf, 'traffic_convention': traffic_convention, 'action_t': action_t, } - on_policy_out = next(iter(on_policy_runner(inputs).values())).cast('float32') - #off_policy_out = next(iter(off_policy_runner(inputs).values())).cast('float32') - return vision_out, on_policy_out - + out = next(iter(model_runner(inputs).values())).cast('float32') + return out, return run_policy -def compile_jit(jit, make_random_inputs, input_keys, frame_skip, vision_metadata, policy_metadata): - vision_input_shapes = vision_metadata['input_shapes'] - policy_input_shapes = policy_metadata['input_shapes'] - +def compile_jit(jit, make_random_inputs, input_keys, make_queues): SEED = 42 def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True): - input_queues, npy = make_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, Device.DEFAULT) - np.random.seed(seed) + input_queues, npy = make_queues(Device.DEFAULT) + rng = np.random.default_rng(seed) Tensor.manual_seed(seed) testing = test_val is not None or test_buffers is not None @@ -214,7 +251,7 @@ def compile_jit(jit, make_random_inputs, input_keys, frame_skip, vision_metadata for i in range(n_runs): for v in npy.values(): - v[:] = np.random.randn(*v.shape).astype(v.dtype) + v[:] = rng.standard_normal(v.shape).astype(v.dtype) Device.default.synchronize() random_inputs = make_random_inputs() st = time.perf_counter() @@ -239,7 +276,10 @@ def compile_jit(jit, make_random_inputs, input_keys, frame_skip, vision_metadata print('capture + replay') test_val, test_buffers = random_inputs_run(jit, SEED) print('pickle round trip') - jit = pickle.loads(pickle.dumps(jit)) + with tempfile.TemporaryFile(dir=".") as f: + dump_oob(jit, f) + f.seek(0) + jit = load_oob(f) random_inputs_run(jit, SEED, test_val, test_buffers, expect_match=True) random_inputs_run(jit, SEED+1, test_val, test_buffers, expect_match=False) return jit @@ -250,14 +290,13 @@ def _parse_size(s): return int(w), int(h) -def read_file_chunked_to_shm(path): - from openpilot.common.file_chunker import read_file_chunked - from openpilot.system.hardware.hw import Paths - shm_path = os.path.join(Paths.shm_path(), os.path.basename(path)) - atexit.register(lambda: os.path.exists(shm_path) and os.remove(shm_path)) - with open(shm_path, 'wb') as f: - f.write(read_file_chunked(path)) - return shm_path +def read_file_chunked_to_disk(path): + from openpilot.common.file_chunker import open_file_chunked + tmp_path = f'{path}.unchunked' + with open(tmp_path, 'wb') as f, open_file_chunked(path) as src: + shutil.copyfileobj(src, f) + atexit.register(lambda: os.path.exists(tmp_path) and os.remove(tmp_path)) + return tmp_path if __name__ == "__main__": @@ -268,32 +307,34 @@ if __name__ == "__main__": p.add_argument('--model-size', type=_parse_size, required=True, help='model input WxH') p.add_argument('--camera-resolutions', type=_parse_size, nargs='+', required=True, help='camera resolutions WxH (one or more)') - p.add_argument('--vision-onnx', required=True) - p.add_argument('--on-policy-onnx', required=True) + p.add_argument('--onnx', required=True) p.add_argument('--output', required=True) p.add_argument('--frame-skip', type=int, required=True) args = p.parse_args() - out = defaultdict(dict) - vision_path, on_policy_path = read_file_chunked_to_shm(args.vision_onnx), read_file_chunked_to_shm(args.on_policy_onnx) + if 'USB+AMD' in os.environ.get('DEV', ''): + wait_usbgpu_link() + + model_path = read_file_chunked_to_disk(args.onnx) model_w, model_h = args.model_size - vision_runner = OnnxRunner(vision_path) - on_policy_runner = OnnxRunner(on_policy_path) - vision_metadata, on_policy_metadata = make_metadata_dict(vision_path), make_metadata_dict(on_policy_path) + model_runner = OnnxRunner(model_path) + out = {'metadata': make_metadata_dict(model_path)} - run_policy_jit = TinyJit(make_run_policy(vision_runner, on_policy_runner, vision_metadata['output_slices']['hidden_state'], args.frame_skip), prune=True) - out['metadata']['vision'], out['metadata']['on_policy'] = vision_metadata, on_policy_metadata + run_policy_jit = TinyJit(make_run_policy(model_runner, out['metadata'], args.frame_skip), prune=True) - make_random_model_inputs = partial(make_random_images, keys=['img', 'big_img'], shape=vision_metadata['input_shapes']['img']) - out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS, args.frame_skip, vision_metadata, on_policy_metadata) + make_policy_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip) + make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, *out['metadata']['input_shapes']['img'][2:]), device=WARP_DEV) + out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS, + make_policy_queues) for cam_w, cam_h in args.camera_resolutions: nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)) make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=WARP_DEV) - warp_enqueue = TinyJit(make_warp(nv12, model_w, model_h, args.frame_skip), prune=True) - out[(cam_w,cam_h)] = compile_jit(warp_enqueue, make_random_warp_inputs, WARP_INPUTS, args.frame_skip, vision_metadata, on_policy_metadata) + warp = TinyJit(make_warp(nv12, model_w, model_h, args.frame_skip), prune=True) + make_warp_queues = partial(make_warp_input_queues, out['metadata']['input_shapes'], args.frame_skip) + out[(cam_w,cam_h)] = compile_jit(warp, make_random_warp_inputs, WARP_INPUTS, make_warp_queues) with open(args.output, "wb") as f: - pickle.dump(out, f) + dump_oob(out, f) print(f"Saved JITs to {args.output} ({os.path.getsize(args.output) / 1e6:.2f} MB)") diff --git a/selfdrive/modeld/constants.py b/openpilot/selfdrive/modeld/constants.py similarity index 100% rename from selfdrive/modeld/constants.py rename to openpilot/selfdrive/modeld/constants.py diff --git a/selfdrive/modeld/dmonitoringmodeld.py b/openpilot/selfdrive/modeld/dmonitoringmodeld.py similarity index 95% rename from selfdrive/modeld/dmonitoringmodeld.py rename to openpilot/selfdrive/modeld/dmonitoringmodeld.py index eaf423e7be..554407a223 100755 --- a/selfdrive/modeld/dmonitoringmodeld.py +++ b/openpilot/selfdrive/modeld/dmonitoringmodeld.py @@ -6,18 +6,18 @@ import time import pickle import numpy as np -from cereal import messaging -from cereal.messaging import PubMaster, SubMaster +from openpilot.cereal import messaging +from openpilot.cereal.messaging import PubMaster, SubMaster from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from openpilot.common.swaglog import cloudlog from openpilot.common.realtime import config_realtime_process from openpilot.common.transformations.model import dmonitoringmodel_intrinsics from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.system.camerad.cameras.nv12_info import get_nv12_info -from openpilot.common.file_chunker import read_file_chunked +from openpilot.common.file_chunker import open_file_chunked from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid, safe_exp -PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld" +PROCESS_NAME = "openpilot.selfdrive.modeld.dmonitoringmodeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') MODEL_PKL_PATH = MODELS_DIR / 'dmonitoring_model_tinygrad.pkl' METADATA_PATH = MODELS_DIR / 'dmonitoring_model_metadata.pkl' @@ -43,7 +43,7 @@ class ModelState: self.frame_buf_params = get_nv12_info(cam_w, cam_h) self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()} self._blob_cache : dict[int, Tensor] = {} - self.model_run = pickle.loads(read_file_chunked(str(MODEL_PKL_PATH))) + self.model_run = pickle.load(open_file_chunked(str(MODEL_PKL_PATH))) with open(MODELS_DIR / f'dm_warp_{cam_w}x{cam_h}_tinygrad.pkl', "rb") as f: self.image_warp = pickle.load(f) diff --git a/selfdrive/modeld/fill_model_msg.py b/openpilot/selfdrive/modeld/fill_model_msg.py similarity index 90% rename from selfdrive/modeld/fill_model_msg.py rename to openpilot/selfdrive/modeld/fill_model_msg.py index 7273745c7b..c4c973bb78 100644 --- a/selfdrive/modeld/fill_model_msg.py +++ b/openpilot/selfdrive/modeld/fill_model_msg.py @@ -1,7 +1,7 @@ import os import capnp import numpy as np -from cereal import log +from openpilot.cereal import log from openpilot.selfdrive.modeld.constants import ModelConstants, Plan, Meta from openpilot.sunnypilot.models.helpers import plan_x_idxs_helper @@ -56,26 +56,29 @@ def fill_lane_line_meta(builder, lane_lines, lane_line_probs): builder.rightY = lane_lines[2].y[0] builder.rightProb = lane_line_probs[2] -def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._DynamicStructBuilder, - net_output_data: dict[str, np.ndarray], action: log.ModelDataV2.Action, +def fill_driving_model_data(msg: capnp._DynamicStructBuilder, modelv2_send: capnp._DynamicStructBuilder) -> None: + msg.valid = modelv2_send.valid + modelV2 = modelv2_send.modelV2 + driving_model_data = msg.drivingModelData + driving_model_data.frameId = modelV2.frameId + driving_model_data.frameIdExtra = modelV2.frameIdExtra + driving_model_data.frameDropPerc = modelV2.frameDropPerc + driving_model_data.modelExecutionTime = modelV2.modelExecutionTime + driving_model_data.action = modelV2.action + driving_model_data.meta.laneChangeState = modelV2.meta.laneChangeState + driving_model_data.meta.laneChangeDirection = modelV2.meta.laneChangeDirection + fill_lane_line_meta(driving_model_data.laneLineMeta, modelV2.laneLines, modelV2.laneLineProbs) + fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, modelV2.position.x, modelV2.position.y, modelV2.position.z) + +def fill_model_msg(msg: capnp._DynamicStructBuilder, net_output_data: dict[str, np.ndarray], action: log.ModelDataV2.Action, publish_state: PublishState, vipc_frame_id: int, vipc_frame_id_extra: int, frame_id: int, frame_drop: float, timestamp_eof: int, model_execution_time: float, valid: bool) -> None: frame_age = frame_id - vipc_frame_id if frame_id > vipc_frame_id else 0 frame_drop_perc = frame_drop * 100 - extended_msg.valid = valid - base_msg.valid = valid + msg.valid = valid - driving_model_data = base_msg.drivingModelData - - driving_model_data.frameId = vipc_frame_id - driving_model_data.frameIdExtra = vipc_frame_id_extra - driving_model_data.frameDropPerc = frame_drop_perc - driving_model_data.modelExecutionTime = model_execution_time - - driving_model_data.action = action - - modelV2 = extended_msg.modelV2 + modelV2 = msg.modelV2 modelV2.frameId = vipc_frame_id modelV2.frameIdExtra = vipc_frame_id_extra modelV2.frameAge = frame_age @@ -90,9 +93,6 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D fill_xyzt(modelV2.orientation, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.T_FROM_CURRENT_EULER].T) fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) - # poly path - fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) - # action modelV2.action = action @@ -107,8 +107,6 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D modelV2.laneLineStds = net_output_data['lane_lines_stds'][0,:,0,0].tolist() modelV2.laneLineProbs = net_output_data['lane_lines_prob'][0,1::2].tolist() - fill_lane_line_meta(driving_model_data.laneLineMeta, modelV2.laneLines, modelV2.laneLineProbs) - # road edges modelV2.init('roadEdges', 2) for i in range(2): diff --git a/selfdrive/modeld/get_model_metadata.py b/openpilot/selfdrive/modeld/get_model_metadata.py similarity index 100% rename from selfdrive/modeld/get_model_metadata.py rename to openpilot/selfdrive/modeld/get_model_metadata.py diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py new file mode 100644 index 0000000000..608bc6aa64 --- /dev/null +++ b/openpilot/selfdrive/modeld/helpers.py @@ -0,0 +1,59 @@ +import io +import json +import pickle +import shutil +import struct +import tempfile +from pathlib import Path + +MODELS_DIR = Path(__file__).resolve().parent / 'models' +TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json' +USBGPU_VID = 0xADD1 +USBGPU_PID = 0x0001 + + +def get_tg_input_devices(process_name: str, usbgpu: bool): + with open(TG_INPUT_DEVICES_PATH) as f: + return json.load(f)[process_name]['default' if not usbgpu else 'usbgpu'] + +def modeld_pkl_path(usbgpu: bool): + prefix = 'big_' if usbgpu else '' + return MODELS_DIR / f'{prefix}driving_tinygrad.pkl' + +def dump_oob(obj, f): + with tempfile.TemporaryFile(dir=".") as tmp: + def buffer_callback(pb: pickle.PickleBuffer): + m = pb.raw() + tmp.write(struct.pack(' bool: + for d in Path("/sys/bus/usb/devices").glob("*"): + try: + if int((d / "idVendor").read_text(), 16) == USBGPU_VID and \ + int((d / "idProduct").read_text(), 16) == USBGPU_PID: + return True + except Exception: + pass + return False diff --git a/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py similarity index 83% rename from selfdrive/modeld/modeld.py rename to openpilot/selfdrive/modeld/modeld.py index 9107cc6228..1ca1a71f35 100755 --- a/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -3,11 +3,11 @@ import os os.environ['GMMU'] = '0' # for usbgpu fast loading, noop for qcom from tinygrad.tensor import Tensor import time -import pickle import numpy as np -import cereal.messaging as messaging -from cereal import car, log -from cereal.messaging import PubMaster, SubMaster +import openpilot.cereal.messaging as messaging +from openpilot.cereal import log +from opendbc.car.structs import car +from openpilot.cereal.messaging import PubMaster, SubMaster from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from opendbc.car.car_helpers import get_demo_car_params from openpilot.common.swaglog import cloudlog @@ -21,15 +21,16 @@ from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS -from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState -from openpilot.common.file_chunker import read_file_chunked, get_manifest_path +from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState +from openpilot.common.file_chunker import open_file_chunked, get_manifest_path from openpilot.selfdrive.modeld.constants import ModelConstants, Plan -from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path, get_tg_input_devices +from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path, get_tg_input_devices, load_oob +from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase -PROCESS_NAME = "selfdrive.modeld.modeld" +PROCESS_NAME = "openpilot.selfdrive.modeld.modeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') LAT_SMOOTH_SECONDS = 0.0 @@ -83,33 +84,29 @@ class ModelState(ModelStateBase): self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu) self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV'] - jits = pickle.loads(read_file_chunked(modeld_pkl_path(usbgpu))) - vision_metadata = jits['metadata']['vision'] - self.vision_input_shapes = vision_metadata['input_shapes'] - self.vision_input_names = list(self.vision_input_shapes.keys()) - self.vision_output_slices = vision_metadata['output_slices'] - - policy_metadata = jits['metadata']['on_policy'] - self.policy_input_shapes = policy_metadata['input_shapes'] - self.policy_output_slices = policy_metadata['output_slices'] + jits = load_oob(open_file_chunked(modeld_pkl_path(usbgpu))) + metadata = jits['metadata'] + self.input_shapes = metadata['input_shapes'] + self.vision_input_names = [k for k in self.input_shapes if 'img' in k] + self.output_slices = metadata['output_slices'] self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ - self.input_queues, self.npy = make_input_queues(self.vision_input_shapes, self.policy_input_shapes, self.frame_skip, device=self.QUEUE_DEV) + self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV) self.full_frames: dict[str, Tensor] = {} - self._blob_cache: dict[int, Tensor] = {} + self._blob_cache: dict[tuple[str, int], Tensor] = {} self.parser = Parser() self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')} self.run_policy = jits['run_policy'] - self.warp_enqueue = jits[(cam_w,cam_h)] + self.warp = jits[(cam_w,cam_h)] def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]: parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()} return parsed_model_outputs def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], - inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: + inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None: for key in bufs.keys(): ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data yuv_size = self.frame_buf_params[key][3] @@ -128,24 +125,18 @@ class ModelState(ModelStateBase): self.npy['tfm'][:,:] = transforms['img'][:,:] self.npy['big_tfm'][:,:] = transforms['big_img'][:,:] - img, big_img = self.warp_enqueue(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames['img'], big_frame=self.full_frames['big_img']) + warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames['img'], big_frame=self.full_frames['big_img']) - if prepare_only: - return None - - vision_output, on_policy_output = self.run_policy( - **{k: self.input_queues[k] for k in POLICY_INPUTS}, img=img, big_img=big_img + outs, = self.run_policy( + **{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped ) - - vision_output = vision_output.numpy().flatten() - on_policy_output = on_policy_output.numpy().flatten() - vision_outputs_dict = self.parser.parse_vision_outputs(self.slice_outputs(vision_output, self.vision_output_slices)) - policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(on_policy_output, self.policy_output_slices)) - combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict} + model_output = outs.numpy()[0] + outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices)) + self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']] if SEND_RAW_PRED: - combined_outputs_dict['raw_pred'] = np.concatenate([vision_output.copy(), on_policy_output.copy()]) - return combined_outputs_dict + outputs_dict['raw_pred'] = model_output.copy() + return outputs_dict def main(demo=False): @@ -158,10 +149,7 @@ def main(demo=False): params.put_bool("UsbGpuPresent", _present) params.put_bool("UsbGpuCompiled", _compiled) - if not USBGPU: - # USB GPU currently saturates a core so can't do this yet, - # also need to move the aux USB interrupts for good timings - config_realtime_process(7, 54) + config_realtime_process(7, 54) # visionipc clients while True: @@ -186,6 +174,8 @@ def main(demo=False): if use_extra_client: cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})") + if USBGPU: + wait_usbgpu_link() st = time.monotonic() cloudlog.warning("loading model") model = ModelState(vipc_client_main.width, vipc_client_main.height, USBGPU) @@ -269,7 +259,8 @@ def main(demo=False): device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics, False).astype(np.float32) - model_transform_extra = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics, True).astype(np.float32) + has_wide_camera = use_extra_client or main_wide_camera + model_transform_extra = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if has_wide_camera else dc.fcam.intrinsics, True).astype(np.float32) live_calib_seen = True traffic_convention = np.zeros(2) @@ -288,9 +279,6 @@ def main(demo=False): run_count = run_count + 1 frame_drop_ratio = frames_dropped / (1 + frames_dropped) - prepare_only = vipc_dropped_frames > 0 - if prepare_only: - cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames") bufs = {name: buf_extra if 'big' in name else buf_main for name in model.vision_input_names} transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.vision_input_names} @@ -305,7 +293,7 @@ def main(demo=False): } mt1 = time.perf_counter() - model_output = model.run(bufs, transforms, inputs, prepare_only) + model_output = model.run(bufs, transforms, inputs) mt2 = time.perf_counter() model_execution_time = mt2 - mt1 @@ -317,7 +305,7 @@ def main(demo=False): action = get_action_from_model(model_output, prev_action, lat_action_t, long_action_t, v_ego) prev_action = action - fill_model_msg(drivingdata_send, modelv2_send, model_output, action, + fill_model_msg(modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen) @@ -329,9 +317,8 @@ def main(demo=False): modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction - drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state - drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction + fill_driving_model_data(drivingdata_send, modelv2_send) fill_pose_msg(posenet_send, model_output, meta_main.frame_id, vipc_dropped_frames, meta_main.timestamp_eof, live_calib_seen) pm.send('modelV2', modelv2_send) pm.send('drivingModelData', drivingdata_send) diff --git a/openpilot/selfdrive/modeld/models/README.md b/openpilot/selfdrive/modeld/models/README.md new file mode 100644 index 0000000000..0055a0f1e3 --- /dev/null +++ b/openpilot/selfdrive/modeld/models/README.md @@ -0,0 +1,2 @@ +## Neural networks in openpilot +To view the architecture of the ONNX networks, you can use [netron](https://netron.app/) diff --git a/selfdrive/modeld/__init__.py b/openpilot/selfdrive/modeld/models/__init__.py similarity index 100% rename from selfdrive/modeld/__init__.py rename to openpilot/selfdrive/modeld/models/__init__.py diff --git a/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx b/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx new file mode 100644 index 0000000000..4a04bd7833 --- /dev/null +++ b/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff +size 1757355221 diff --git a/selfdrive/modeld/models/dmonitoring_model.onnx b/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx similarity index 100% rename from selfdrive/modeld/models/dmonitoring_model.onnx rename to openpilot/selfdrive/modeld/models/dmonitoring_model.onnx diff --git a/openpilot/selfdrive/modeld/models/driving_supercombo.onnx b/openpilot/selfdrive/modeld/models/driving_supercombo.onnx new file mode 100644 index 0000000000..f0672eab48 --- /dev/null +++ b/openpilot/selfdrive/modeld/models/driving_supercombo.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:659727c4d4839adc4992a254409a54259a8756a743f2d567bf5fdc6579f8009b +size 60881999 diff --git a/selfdrive/modeld/parse_model_outputs.py b/openpilot/selfdrive/modeld/parse_model_outputs.py similarity index 98% rename from selfdrive/modeld/parse_model_outputs.py rename to openpilot/selfdrive/modeld/parse_model_outputs.py index 26c138b8ec..839c20f7cc 100644 --- a/selfdrive/modeld/parse_model_outputs.py +++ b/openpilot/selfdrive/modeld/parse_model_outputs.py @@ -41,7 +41,7 @@ class Parser: raw = outs[name] outs[name] = sigmoid(raw) - def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None): + def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=()): if self.check_missing(outs, name): return raw = outs[name] diff --git a/openpilot/selfdrive/modeld/usbgpu_link.py b/openpilot/selfdrive/modeld/usbgpu_link.py new file mode 100644 index 0000000000..ef12cba939 --- /dev/null +++ b/openpilot/selfdrive/modeld/usbgpu_link.py @@ -0,0 +1,34 @@ +import time +from pathlib import Path + +from openpilot.common.swaglog import cloudlog +from openpilot.common.hardware.usb import CHESTNUT_VENDOR_ID, CHESTNUT_PRODUCT_ID, usb_devices, controller, read_int + +STABLE_SECONDS = 2.0 +STABLE_THRESHOLD = 5.0 # link errors per second + + +def _chestnut_portli() -> Path | None: + for device in usb_devices(): + if read_int(device / "idVendor", 16) == CHESTNUT_VENDOR_ID and \ + read_int(device / "idProduct", 16) == CHESTNUT_PRODUCT_ID: + ctrl = controller(device) + if ctrl is not None and (ctrl / "portli").exists(): + return ctrl / "portli" + return None + + +def wait_usbgpu_link(timeout: float = 30.0) -> None: + portli = _chestnut_portli() + if portli is None: + return + + t0 = time.monotonic() + while time.monotonic() - t0 < timeout: + start = read_int(portli, 0) + time.sleep(STABLE_SECONDS) + rate = (read_int(portli, 0) - start) / STABLE_SECONDS + if rate <= STABLE_THRESHOLD: + return + cloudlog.warning(f"usbgpu link not stable: {rate:.0f} errors/s") + cloudlog.error("usbgpu link never stabilized") diff --git a/selfdrive/monitoring/dmonitoringd.py b/openpilot/selfdrive/monitoring/dmonitoringd.py similarity index 97% rename from selfdrive/monitoring/dmonitoringd.py rename to openpilot/selfdrive/monitoring/dmonitoringd.py index bedad967b6..085a13fcb1 100755 --- a/selfdrive/monitoring/dmonitoringd.py +++ b/openpilot/selfdrive/monitoring/dmonitoringd.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process from openpilot.selfdrive.monitoring.policy import DriverMonitoring diff --git a/selfdrive/monitoring/policy.py b/openpilot/selfdrive/monitoring/policy.py similarity index 85% rename from selfdrive/monitoring/policy.py rename to openpilot/selfdrive/monitoring/policy.py index 728d4881cd..1489e9c798 100644 --- a/selfdrive/monitoring/policy.py +++ b/openpilot/selfdrive/monitoring/policy.py @@ -2,8 +2,9 @@ from collections import defaultdict from math import atan2, radians import numpy as np -from cereal import car, log -import cereal.messaging as messaging +from openpilot.cereal import log +from opendbc.car.structs import car +import openpilot.cereal.messaging as messaging from openpilot.common.realtime import DT_DMON from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params @@ -24,21 +25,27 @@ def to_percent(v): class DRIVER_MONITOR_SETTINGS: def __init__(self): - # https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:42018X1947&rid=2 - self._WHEELTOUCH_POLICY_ALERT_1_TIMEOUT = 15. - self._WHEELTOUCH_POLICY_ALERT_2_TIMEOUT = 24. - self._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT = 30. - # https://cdn.euroncap.com/cars/assets/euro_ncap_protocol_safe_driving_driver_engagement_v11_a30e874152.pdf - self._VISION_POLICY_ALERT_1_TIMEOUT = 3. - self._VISION_POLICY_ALERT_2_TIMEOUT = 5. - self._VISION_POLICY_ALERT_3_TIMEOUT = 11. + # https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=OJ:L_202501899 + self._ALERT_MIN_SPEED = 2.8 # 10 km/h + + self._WHEELTOUCH_POLICY_ALERT_1_TIMEOUT = 5. + self._WHEELTOUCH_POLICY_ALERT_2_TIMEOUT = 15. + self._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT = 25. + self._VISION_POLICY_ALERT_1_TIMEOUT = 5. + self._VISION_POLICY_ALERT_2_TIMEOUT = 8. + self._VISION_POLICY_ALERT_3_TIMEOUT = 13. + + # no response = alert_3 sustained for certain amount of time + self._NO_RESPONSE_TIMEOUT = 5. + + # lockout specs + self._MAX_ALERT_3 = 2 + self._MAX_NO_RESPONSE = 1 + self._LOCKOUT_TIMES = [int(60 * n_min / DT_DMON) for n_min in [1, 5, 15, 30]] self._TIMEOUT_RECOVERY_FACTOR_MAX = 5. self._TIMEOUT_RECOVERY_FACTOR_MIN = 1.25 - self._MAX_TERMINAL_ALERTS = 3 # not allowed to engage after 3 terminal alerts - self._MAX_TERMINAL_DURATION = int(30 / DT_DMON) # not allowed to engage after 30s of terminal alerts - self._FACE_THRESHOLD = 0.7 self._EYE_THRESHOLD = 0.65 self._SG_THRESHOLD = 0.9 @@ -141,8 +148,14 @@ class DriverMonitoring: self.wheel_on_right_last = None self.wheel_on_right_default = rhd_saved self.face_detected = False - self.terminal_alert_cnt = 0 - self.terminal_time = 0 + self.alert_3_cnt = 0 + self.cnt_since_alert_3 = 0 + self.no_response_timeout = int(self.settings._NO_RESPONSE_TIMEOUT / DT_DMON) + self.no_response_cnt = 0 + self.lockout_active = Params().get_bool("DriverTooDistracted") + self.lockout_count = Params().get("DriverLockoutCount") or 0 + self.lockout_duration = self.settings._LOCKOUT_TIMES[min(max(self.lockout_count - 1, 0), len(self.settings._LOCKOUT_TIMES) - 1)] + self.lockout_time_elapsed = 0 self.step_change = 0. self.active_policy = MonitoringPolicy.vision self.driver_interacting = False @@ -153,7 +166,6 @@ class DriverMonitoring: self.threshold_alert_2 = 0. self.dcam_uncertain_cnt = 0 self.dcam_reset_cnt = 0 - self.too_distracted = Params().get_bool("DriverTooDistracted") self._reset_awareness() self._set_policy(MonitoringPolicy.vision) @@ -229,7 +241,7 @@ class DriverMonitoring: self.distracted_types['eye'] = bool((self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD) self.distracted_types['phone'] = bool(self.phone_prob > self.settings._PHONE_THRESH) - def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged, standstill, demo_mode=False, steering_angle_deg=0.): + def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged, lowspeed, demo_mode=False, steering_angle_deg=0.): rhd_pred = driver_state.wheelOnRightProb # calibrates only when there's movement and either face detected if car_speed > self.settings._WHEELPOS_CALIB_MIN_SPEED and (driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD or @@ -280,7 +292,7 @@ class DriverMonitoring: if self.face_detected and not self.driver_distracted: dcam_uncertain = self.model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD - if dcam_uncertain and not standstill: + if dcam_uncertain and not lowspeed: self.dcam_uncertain_cnt += 1 self.dcam_reset_cnt = 0 else: @@ -295,13 +307,25 @@ class DriverMonitoring: elif self.face_detected and self.pose.low_std: self.hi_stds = 0 - def _update_events(self, driver_engaged, op_engaged, standstill, wrong_gear): + def _update_events(self, driver_engaged, op_engaged, lowspeed, wrong_gear): self.alert_level = AlertLevel.none self.driver_interacting = driver_engaged - if self.terminal_alert_cnt >= self.settings._MAX_TERMINAL_ALERTS or \ - self.terminal_time >= self.settings._MAX_TERMINAL_DURATION: - self.too_distracted = True + if self.alert_3_cnt >= self.settings._MAX_ALERT_3 or self.no_response_cnt >= self.settings._MAX_NO_RESPONSE: + if not self.lockout_active: + self.lockout_count += 1 + self.lockout_duration = self.settings._LOCKOUT_TIMES[min(self.lockout_count - 1, len(self.settings._LOCKOUT_TIMES) - 1)] + Params().put("DriverLockoutCount", self.lockout_count) + self.lockout_active = True + + if self.lockout_active: + self.lockout_time_elapsed += 1 + if self.lockout_time_elapsed > self.lockout_duration: + self.lockout_active = False + self.alert_3_cnt = 0 + self.cnt_since_alert_3 = 0 + self.no_response_cnt = 0 + self.lockout_time_elapsed = 0 always_on_valid = self.always_on and not wrong_gear if (self.driver_interacting and self.awareness > 0 and self.active_policy == MonitoringPolicy.wheeltouch) or \ @@ -314,11 +338,11 @@ class DriverMonitoring: awareness_prev = self.awareness _reaching_alert_1 = self.awareness - self.step_change <= self.threshold_alert_1 _reaching_alert_3 = self.awareness - self.step_change <= 0 - standstill_exemption = standstill and _reaching_alert_1 + lowspeed_exemption = lowspeed and _reaching_alert_1 always_on_exemption = always_on_valid and not op_engaged and _reaching_alert_3 if self.awareness > 0 and \ - ((self.driver_distraction_filter.x < 0.37 and self.face_detected and self.pose.low_std) or standstill_exemption): + ((self.driver_distraction_filter.x < 0.37 and self.face_detected and self.pose.low_std) or lowspeed_exemption): if self.driver_interacting: self._reset_awareness() return @@ -335,30 +359,39 @@ class DriverMonitoring: maybe_distracted = self.is_model_uncertain or not self.face_detected if certainly_distracted or maybe_distracted: - # should always be counting if distracted unless at standstill and reaching green + # should always be counting if distracted unless at low speed and reaching green # also will not be reaching 0 if DM is active when not engaged - if not (standstill_exemption or always_on_exemption): + if not (lowspeed_exemption or always_on_exemption): self.awareness = max(self.awareness - self.step_change, -0.1) if self.awareness <= 0.: # terminal alert: disengagement required self.alert_level = AlertLevel.three - self.terminal_time += 1 if awareness_prev > 0.: - self.terminal_alert_cnt += 1 - elif self.awareness <= self.threshold_alert_2: - self.alert_level = AlertLevel.two - elif self.awareness <= self.threshold_alert_1: - self.alert_level = AlertLevel.one + self.alert_3_cnt += 1 + self.cnt_since_alert_3 = 0 + else: + self.cnt_since_alert_3 += 1 + if self.cnt_since_alert_3 == self.no_response_timeout: + self.no_response_cnt += 1 + else: + if self.awareness <= self.threshold_alert_2: + self.alert_level = AlertLevel.two + elif self.awareness <= self.threshold_alert_1: + self.alert_level = AlertLevel.one def get_state_packet(self, valid=True): # build driverMonitoringState packet dat = messaging.new_message('driverMonitoringState', valid=valid) dm = dat.driverMonitoringState - dm.lockout = self.too_distracted - dm.alertCountLockoutPercent = to_percent(self.terminal_alert_cnt / self.settings._MAX_TERMINAL_ALERTS) - dm.alertTimeLockoutPercent = to_percent(self.terminal_time / self.settings._MAX_TERMINAL_DURATION) + dm.lockout = self.lockout_active + dm.lockoutCount = self.lockout_count + if self.lockout_active: + dm.lockoutMinutesRemaining = max(1, round((self.lockout_duration - self.lockout_time_elapsed) * DT_DMON / 60.)) + dm.alert3Count = self.alert_3_cnt + dm.noResponseCount = self.no_response_cnt + dm.noResponseForceDecel = self.alert_level == AlertLevel.three and self.cnt_since_alert_3 >= self.no_response_timeout dm.alwaysOn = self.always_on dm.alwaysOnLockout = self.always_on and self.awareness <= self.threshold_alert_2 dm.alertLevel = self.alert_level @@ -395,7 +428,7 @@ class DriverMonitoring: car_speed = 30 enabled = True wrong_gear = False - standstill = False + lowspeed = False driver_engaged = False brake_disengage_prob = 1.0 steering_angle_deg = 0.0 @@ -404,7 +437,7 @@ class DriverMonitoring: car_speed = sm['carState'].vEgo enabled = sm['selfdriveState'].enabled or sm['carControl'].latActive wrong_gear = sm['carState'].gearShifter not in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low) - standstill = sm['carState'].standstill + lowspeed = car_speed < self.settings._ALERT_MIN_SPEED driver_engaged = sm['carState'].steeringPressed or (sm['selfdriveState'].enabled and sm['carState'].gasPressed) brake_disengage_prob = sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs[0] # brake disengage prob in next 2s steering_angle_deg = sm['carState'].steeringAngleDeg @@ -421,7 +454,7 @@ class DriverMonitoring: cal_rpy=rpyCalib, car_speed=car_speed, op_engaged=enabled, - standstill=standstill, + lowspeed=lowspeed, demo_mode=demo, steering_angle_deg=steering_angle_deg, ) @@ -430,6 +463,6 @@ class DriverMonitoring: self._update_events( driver_engaged=driver_engaged, op_engaged=enabled, - standstill=standstill, + lowspeed=lowspeed, wrong_gear=wrong_gear, ) diff --git a/selfdrive/monitoring/test_monitoring.py b/openpilot/selfdrive/monitoring/test_monitoring.py similarity index 81% rename from selfdrive/monitoring/test_monitoring.py rename to openpilot/selfdrive/monitoring/test_monitoring.py index f933facdee..accf496b47 100644 --- a/selfdrive/monitoring/test_monitoring.py +++ b/openpilot/selfdrive/monitoring/test_monitoring.py @@ -1,7 +1,7 @@ -import numpy as np import pytest -from cereal import log, car +from openpilot.cereal import log +from opendbc.car.structs import car from openpilot.common.realtime import DT_DMON from openpilot.selfdrive.monitoring.policy import DriverMonitoring, DRIVER_MONITOR_SETTINGS @@ -50,15 +50,15 @@ always_true = [True] * int(TEST_TIMESPAN / DT_DMON) always_false = [False] * int(TEST_TIMESPAN / DT_DMON) class TestMonitoring: - def _run_seq(self, msgs, interaction, engaged, standstill): + def _run_seq(self, msgs, interaction, engaged, lowspeed): DM = DriverMonitoring() alert_lvls = [] for idx in range(len(msgs)): - DM._update_states(msgs[idx], [0, 0, 0], 0, engaged[idx], standstill[idx]) + DM._update_states(msgs[idx], [0, 0, 0], 0, engaged[idx], lowspeed[idx]) # cal_rpy and car_speed don't matter here # evaluate events at 10Hz for tests - DM._update_events(interaction[idx], engaged[idx], standstill[idx], 0) + DM._update_events(interaction[idx], engaged[idx], lowspeed[idx], 0) alert_lvls.append(DM.alert_level) assert len(alert_lvls) == len(msgs), f"got {len(alert_lvls)} for {len(msgs)} driverState input msgs" return alert_lvls, DM @@ -83,6 +83,21 @@ class TestMonitoring: (TEST_TIMESPAN - 10 - s._VISION_POLICY_ALERT_3_TIMEOUT) / 2) / DT_DMON)] == 3 assert isinstance(d_status.awareness, float) + # engaged, distracted past red and beyond the no-response window -> unavailability response + lockout + def test_distracted_lockout(self): + alert_lvls, d_status = self._run_seq(always_distracted, always_false, always_true, always_false) + assert alert_lvls[int(DISTRACTED_SECONDS_TO_RED / DT_DMON)] == 3 + assert d_status.lockout_active + assert d_status.lockout_time_elapsed > 0 + assert d_status.lockout_count >= 1 + + # no face -> wheeltouch red, sustained past the no-response timeout -> unavailability response + lockout + def test_invisible_lockout(self): + _, d_status = self._run_seq(always_no_face, always_false, always_true, always_false) + assert d_status.active_policy == log.DriverMonitoringState.MonitoringPolicy.wheeltouch + assert d_status.lockout_active + assert d_status.lockout_count >= 1 + # engaged, no face detected the whole time, no action def test_fully_invisible_driver(self): alert_lvls, d_status = self._run_seq(always_no_face, always_false, always_true, always_false) @@ -138,22 +153,22 @@ class TestMonitoring: # engaged, invisible driver, down to orange, driver touches wheel; then down to orange again, driver appears # - both actions should clear the alert, but momentary appearance should not def test_sometimes_transparent_commuter(self): - _visible_time = np.random.choice([0.5, 10]) - ds_vector = always_no_face[:]*2 - interaction_vector = always_false[:]*2 - ds_vector[int((2*INVISIBLE_SECONDS_TO_ORANGE+1)/DT_DMON):int((2*INVISIBLE_SECONDS_TO_ORANGE+1+_visible_time)/DT_DMON)] = \ - [msg_ATTENTIVE] * int(_visible_time/DT_DMON) - interaction_vector[int((INVISIBLE_SECONDS_TO_ORANGE)/DT_DMON):int((INVISIBLE_SECONDS_TO_ORANGE+1)/DT_DMON)] = [True] * int(1/DT_DMON) - alert_lvls, _ = self._run_seq(ds_vector, interaction_vector, 2*always_true, 2*always_false) - assert alert_lvls[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)] == 0 - assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)] == 2 - assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE+0.1)/DT_DMON)] == 0 - if _visible_time == 0.5: - assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)] == 2 - assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)] == 1 - elif _visible_time == 10: - assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)] == 2 - assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)] == 0 + for _visible_time in (0.5, 10): + ds_vector = always_no_face[:]*2 + interaction_vector = always_false[:]*2 + ds_vector[int((2*INVISIBLE_SECONDS_TO_ORANGE+1)/DT_DMON):int((2*INVISIBLE_SECONDS_TO_ORANGE+1+_visible_time)/DT_DMON)] = \ + [msg_ATTENTIVE] * int(_visible_time/DT_DMON) + interaction_vector[int((INVISIBLE_SECONDS_TO_ORANGE)/DT_DMON):int((INVISIBLE_SECONDS_TO_ORANGE+1)/DT_DMON)] = [True] * int(1/DT_DMON) + alert_lvls, _ = self._run_seq(ds_vector, interaction_vector, 2*always_true, 2*always_false) + assert alert_lvls[int(dm_settings._WHEELTOUCH_POLICY_ALERT_1_TIMEOUT/2/DT_DMON)] == 0 + assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)] == 2 + assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE+0.1)/DT_DMON)] == 0 + if _visible_time == 0.5: + assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)] == 2 + assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)] == 2 + elif _visible_time == 10: + assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)] == 2 + assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)] == 0 # engaged, invisible driver, down to red, driver appears and then touches wheel, then disengages/reengages # - only disengage will clear the alert @@ -166,7 +181,7 @@ class TestMonitoring: interaction_vector[int((INVISIBLE_SECONDS_TO_RED+_visible_time)/DT_DMON):int((INVISIBLE_SECONDS_TO_RED+_visible_time+1)/DT_DMON)] = [True] * int(1/DT_DMON) op_vector[int((INVISIBLE_SECONDS_TO_RED+_visible_time+1)/DT_DMON):int((INVISIBLE_SECONDS_TO_RED+_visible_time+0.5)/DT_DMON)] = [False] * int(0.5/DT_DMON) alert_lvls, _ = self._run_seq(ds_vector, interaction_vector, op_vector, always_false) - assert alert_lvls[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)] == 0 + assert alert_lvls[int(dm_settings._WHEELTOUCH_POLICY_ALERT_1_TIMEOUT/2/DT_DMON)] == 0 assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)] == 2 assert alert_lvls[int((INVISIBLE_SECONDS_TO_RED-0.1)/DT_DMON)] == 3 assert alert_lvls[int((INVISIBLE_SECONDS_TO_RED+0.5*_visible_time)/DT_DMON)] == 3 @@ -183,9 +198,9 @@ class TestMonitoring: # - should only reach green when stopped, but continues counting down on launch def test_long_traffic_light_victim(self): _redlight_time = 60 # seconds - standstill_vector = always_true[:] - standstill_vector[int(_redlight_time/DT_DMON):] = [False] * int((TEST_TIMESPAN-_redlight_time)/DT_DMON) - alert_lvls, d_status = self._run_seq(always_distracted, always_false, always_true, standstill_vector) + lowspeed_vector = always_true[:] + lowspeed_vector[int(_redlight_time/DT_DMON):] = [False] * int((TEST_TIMESPAN-_redlight_time)/DT_DMON) + alert_lvls, d_status = self._run_seq(always_distracted, always_false, always_true, lowspeed_vector) s = d_status.settings assert alert_lvls[int((_redlight_time-0.1)/DT_DMON)] == 0 _alert_1_to_2 = s._VISION_POLICY_ALERT_2_TIMEOUT - s._VISION_POLICY_ALERT_1_TIMEOUT @@ -193,12 +208,12 @@ class TestMonitoring: assert alert_lvls[int((_redlight_time+_alert_1_to_2+0.5)/DT_DMON)] == 2 # engaged, distracted while moving, then car stops after reaching orange - # - should reset timer to pre green at standstill + # - should reset timer to pre green at low speed def test_distracted_then_stops(self): _stop_time = DISTRACTED_SECONDS_TO_ORANGE + 1 # stop 1 second after reaching orange - standstill_vector = always_false[:] - standstill_vector[int(_stop_time/DT_DMON):] = [True] * int((TEST_TIMESPAN-_stop_time)/DT_DMON) - alert_lvls, _ = self._run_seq(always_distracted, always_false, always_true, standstill_vector) + lowspeed_vector = always_false[:] + lowspeed_vector[int(_stop_time/DT_DMON):] = [True] * int((TEST_TIMESPAN-_stop_time)/DT_DMON) + alert_lvls, _ = self._run_seq(always_distracted, always_false, always_true, lowspeed_vector) # just before and briefly after stopping: orange alert; goes away quickly after stopped assert alert_lvls[int((_stop_time+0.1)/DT_DMON)] == 2 assert alert_lvls[int((_stop_time+0.5)/DT_DMON)] == 0 @@ -251,12 +266,12 @@ def test_run_step_engagement(selfdrive_enabled, lat_active, steering, gas, captured = {} orig = dm._update_events - def spy(driver_engaged, op_engaged, standstill, wrong_gear): + def spy(driver_engaged, op_engaged, lowspeed, wrong_gear): captured['driver_engaged'] = driver_engaged captured['op_engaged'] = op_engaged - return orig(driver_engaged, op_engaged, standstill, wrong_gear) + return orig(driver_engaged, op_engaged, lowspeed, wrong_gear) - dm._update_events = spy + object.__setattr__(dm, '_update_events', spy) dm.run_step(sm, demo=False) assert captured['op_engaged'] == expected_op_engaged assert captured['driver_engaged'] == expected_driver_engaged diff --git a/selfdrive/pandad/.gitignore b/openpilot/selfdrive/pandad/.gitignore similarity index 100% rename from selfdrive/pandad/.gitignore rename to openpilot/selfdrive/pandad/.gitignore diff --git a/selfdrive/pandad/SConscript b/openpilot/selfdrive/pandad/SConscript similarity index 100% rename from selfdrive/pandad/SConscript rename to openpilot/selfdrive/pandad/SConscript diff --git a/selfdrive/pandad/__init__.py b/openpilot/selfdrive/pandad/__init__.py similarity index 100% rename from selfdrive/pandad/__init__.py rename to openpilot/selfdrive/pandad/__init__.py diff --git a/selfdrive/pandad/main.cc b/openpilot/selfdrive/pandad/main.cc similarity index 93% rename from selfdrive/pandad/main.cc rename to openpilot/selfdrive/pandad/main.cc index ef30d6037c..08e88b6163 100644 --- a/selfdrive/pandad/main.cc +++ b/openpilot/selfdrive/pandad/main.cc @@ -3,7 +3,7 @@ #include "selfdrive/pandad/pandad.h" #include "common/swaglog.h" #include "common/util.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" int main(int argc, char *argv[]) { LOGW("starting pandad"); diff --git a/selfdrive/pandad/panda.cc b/openpilot/selfdrive/pandad/panda.cc similarity index 98% rename from selfdrive/pandad/panda.cc rename to openpilot/selfdrive/pandad/panda.cc index acb472ab57..cb5266d253 100644 --- a/selfdrive/pandad/panda.cc +++ b/openpilot/selfdrive/pandad/panda.cc @@ -6,7 +6,7 @@ #include #include -#include "cereal/messaging/messaging.h" +#include "openpilot/cereal/messaging/messaging.h" #include "common/swaglog.h" #include "common/util.h" @@ -116,7 +116,7 @@ std::optional Panda::get_serial() { bool Panda::up_to_date() { if (auto fw_sig = get_firmware_version()) { for (auto fn : { "panda.bin.signed", "panda_h7.bin.signed" }) { - auto content = util::read_file(std::string("../../panda/board/obj/") + fn); + auto content = util::read_file(std::string("../../../panda/board/obj/") + fn); if (content.size() >= fw_sig->size() && memcmp(content.data() + content.size() - fw_sig->size(), fw_sig->data(), fw_sig->size()) == 0) { return true; diff --git a/selfdrive/pandad/panda.h b/openpilot/selfdrive/pandad/panda.h similarity index 96% rename from selfdrive/pandad/panda.h rename to openpilot/selfdrive/pandad/panda.h index 9877e5f739..1c5e4a5504 100644 --- a/selfdrive/pandad/panda.h +++ b/openpilot/selfdrive/pandad/panda.h @@ -9,8 +9,8 @@ #include #include -#include "cereal/gen/cpp/car.capnp.h" -#include "cereal/gen/cpp/log.capnp.h" +#include "openpilot/cereal/gen/cpp/car.capnp.h" +#include "openpilot/cereal/gen/cpp/log.capnp.h" #include "panda/board/health.h" #include "panda/board/can.h" #include "selfdrive/pandad/panda_comms.h" diff --git a/selfdrive/pandad/panda_comms.h b/openpilot/selfdrive/pandad/panda_comms.h similarity index 100% rename from selfdrive/pandad/panda_comms.h rename to openpilot/selfdrive/pandad/panda_comms.h diff --git a/selfdrive/pandad/panda_safety.cc b/openpilot/selfdrive/pandad/panda_safety.cc similarity index 98% rename from selfdrive/pandad/panda_safety.cc rename to openpilot/selfdrive/pandad/panda_safety.cc index 8381256f46..bcec99190a 100644 --- a/selfdrive/pandad/panda_safety.cc +++ b/openpilot/selfdrive/pandad/panda_safety.cc @@ -1,5 +1,5 @@ #include "selfdrive/pandad/pandad.h" -#include "cereal/messaging/messaging.h" +#include "openpilot/cereal/messaging/messaging.h" #include "common/swaglog.h" void PandaSafety::configureSafetyMode(bool is_onroad) { diff --git a/selfdrive/pandad/pandad.cc b/openpilot/selfdrive/pandad/pandad.cc similarity index 91% rename from selfdrive/pandad/pandad.cc rename to openpilot/selfdrive/pandad/pandad.cc index a771326f9d..250ad5a481 100644 --- a/selfdrive/pandad/pandad.cc +++ b/openpilot/selfdrive/pandad/pandad.cc @@ -1,7 +1,6 @@ #include "selfdrive/pandad/pandad.h" #include -#include #include #include #include @@ -9,14 +8,14 @@ #include #include -#include "cereal/gen/cpp/car.capnp.h" -#include "cereal/messaging/messaging.h" -#include "cereal/services.h" +#include "openpilot/cereal/gen/cpp/car.capnp.h" +#include "openpilot/cereal/messaging/messaging.h" +#include "openpilot/cereal/services.h" #include "common/ratekeeper.h" #include "common/swaglog.h" #include "common/timing.h" #include "common/util.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #define MAX_IR_PANDA_VAL 50 #define CUTOFF_IL 400 @@ -24,14 +23,6 @@ ExitHandler do_exit; -struct HwmonState { - std::atomic voltage{0}; - std::atomic current{0}; - std::atomic initialized{false}; -}; - -HwmonState hwmon_state; - bool check_connected(Panda *panda) { if (!panda->connected()) { do_exit = true; @@ -126,26 +117,6 @@ void can_recv(Panda *panda, PubMaster *pm) { } } -void hwmon_thread() { - util::set_thread_name("pandad_hwmon"); - - while (!do_exit) { - double read_time = millis_since_boot(); - uint32_t voltage = Hardware::get_voltage(); - uint32_t current = Hardware::get_current(); - read_time = millis_since_boot() - read_time; - if (read_time > 50) { - LOGW("reading hwmon took %lfms", read_time); - } - - hwmon_state.voltage.store(voltage); - hwmon_state.current.store(current); - hwmon_state.initialized.store(true); - - util::sleep_for(500); - } -} - void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::PandaType hw_type, const health_t &health) { ps.setVoltage(health.voltage_pkt); ps.setCurrent(health.current_pkt); @@ -278,7 +249,8 @@ std::optional send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa } void send_peripheral_state(Panda *panda, PubMaster *pm) { - if (!hwmon_state.initialized.load()) { + auto health_opt = panda->get_state(); + if (!health_opt) { return; } @@ -290,18 +262,9 @@ void send_peripheral_state(Panda *panda, PubMaster *pm) { auto ps = evt.initPeripheralState(); ps.setPandaType(panda->hw_type); - ps.setVoltage(hwmon_state.voltage.load()); - ps.setCurrent(hwmon_state.current.load()); - - // fall back to panda's voltage and current measurement - if (ps.getVoltage() == 0 && ps.getCurrent() == 0) { - auto health_opt = panda->get_state(); - if (health_opt) { - health_t health = *health_opt; - ps.setVoltage(health.voltage_pkt); - ps.setCurrent(health.current_pkt); - } - } + health_t health = *health_opt; + ps.setVoltage(health.voltage_pkt); + ps.setCurrent(health.current_pkt); uint16_t fan_speed_rpm = panda->get_fan_speed(); ps.setFanSpeedRpm(fan_speed_rpm); @@ -413,9 +376,8 @@ void pandad_run(Panda *panda) { const bool spoofing_started = getenv("STARTED") != nullptr; const bool fake_send = getenv("FAKESEND") != nullptr; - // Start helper threads for event-driven sendcan and slow non-Panda reads. + // Start helper thread for event-driven sendcan. std::thread send_thread(can_send_thread, panda, fake_send); - std::thread hardware_thread(hwmon_thread); RateKeeper rk("pandad", 100); SubMaster sm({"selfdriveState", "deviceState", "selfdriveStateSP"}); @@ -475,7 +437,6 @@ void pandad_run(Panda *panda) { } send_thread.join(); - hardware_thread.join(); } void pandad_main_thread(std::string serial) { diff --git a/selfdrive/pandad/pandad.h b/openpilot/selfdrive/pandad/pandad.h similarity index 100% rename from selfdrive/pandad/pandad.h rename to openpilot/selfdrive/pandad/pandad.h diff --git a/selfdrive/pandad/pandad.py b/openpilot/selfdrive/pandad/pandad.py similarity index 98% rename from selfdrive/pandad/pandad.py rename to openpilot/selfdrive/pandad/pandad.py index 6846abcadd..a618dacccd 100755 --- a/selfdrive/pandad/pandad.py +++ b/openpilot/selfdrive/pandad/pandad.py @@ -9,7 +9,7 @@ import subprocess 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.hardware import HARDWARE from openpilot.common.swaglog import cloudlog from openpilot.sunnypilot.selfdrive.pandad.rivian_long_flasher import flash_rivian_long @@ -129,7 +129,7 @@ def main() -> None: # run real pandad os.environ['MANAGER_DAEMON'] = 'pandad' - process = subprocess.Popen(["./pandad"], cwd=os.path.join(BASEDIR, "selfdrive/pandad")) + process = subprocess.Popen(["./pandad"], cwd=os.path.join(BASEDIR, "openpilot/selfdrive/pandad")) process.wait() # TODO: wrap all panda exceptions in a base panda exception except (usb1.USBErrorNoDevice, usb1.USBErrorPipe): diff --git a/selfdrive/pandad/pandad_api_impl.py b/openpilot/selfdrive/pandad/pandad_api_impl.py similarity index 98% rename from selfdrive/pandad/pandad_api_impl.py rename to openpilot/selfdrive/pandad/pandad_api_impl.py index 75a7ba484e..9da277d73a 100644 --- a/selfdrive/pandad/pandad_api_impl.py +++ b/openpilot/selfdrive/pandad/pandad_api_impl.py @@ -1,5 +1,5 @@ import time -from cereal import log +from openpilot.cereal import log NO_TRAVERSAL_LIMIT = 2**64 - 1 diff --git a/selfdrive/pandad/spi.cc b/openpilot/selfdrive/pandad/spi.cc similarity index 100% rename from selfdrive/pandad/spi.cc rename to openpilot/selfdrive/pandad/spi.cc diff --git a/selfdrive/modeld/models/__init__.py b/openpilot/selfdrive/pandad/tests/__init__.py similarity index 100% rename from selfdrive/modeld/models/__init__.py rename to openpilot/selfdrive/pandad/tests/__init__.py diff --git a/selfdrive/pandad/tests/bootstub.panda_h7.bin b/openpilot/selfdrive/pandad/tests/bootstub.panda_h7.bin similarity index 100% rename from selfdrive/pandad/tests/bootstub.panda_h7.bin rename to openpilot/selfdrive/pandad/tests/bootstub.panda_h7.bin diff --git a/selfdrive/pandad/tests/test_pandad.py b/openpilot/selfdrive/pandad/tests/test_pandad.py similarity index 90% rename from selfdrive/pandad/tests/test_pandad.py rename to openpilot/selfdrive/pandad/tests/test_pandad.py index 9dacd3f5a8..0f8fd9fc1a 100644 --- a/selfdrive/pandad/tests/test_pandad.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad.py @@ -2,13 +2,13 @@ import os import pytest import time -import cereal.messaging as messaging -from cereal import log +import openpilot.cereal.messaging as messaging +from openpilot.cereal import log from openpilot.common.gpio import gpio_set, gpio_init from panda import Panda, PandaDFU from openpilot.system.manager.process_config import managed_processes -from openpilot.system.hardware import HARDWARE -from openpilot.system.hardware.tici.pins import GPIO +from openpilot.common.hardware import HARDWARE +from openpilot.common.hardware.tici.pins import GPIO HERE = os.path.dirname(os.path.realpath(__file__)) @@ -60,7 +60,7 @@ class TestPandad: def test_in_reset(self): gpio_init(GPIO.STM_RST_N, True) - gpio_set(GPIO.STM_RST_N, 1) + gpio_set(GPIO.STM_RST_N, True) assert not Panda.list() self._run_test() diff --git a/selfdrive/pandad/tests/test_pandad_canprotocol.cc b/openpilot/selfdrive/pandad/tests/test_pandad_canprotocol.cc similarity index 98% rename from selfdrive/pandad/tests/test_pandad_canprotocol.cc rename to openpilot/selfdrive/pandad/tests/test_pandad_canprotocol.cc index 8499a1aab8..83339a4c1c 100644 --- a/selfdrive/pandad/tests/test_pandad_canprotocol.cc +++ b/openpilot/selfdrive/pandad/tests/test_pandad_canprotocol.cc @@ -4,7 +4,7 @@ #include #include "catch2/catch.hpp" -#include "cereal/messaging/messaging.h" +#include "openpilot/cereal/messaging/messaging.h" #include "common/util.h" #include "selfdrive/pandad/panda.h" diff --git a/selfdrive/pandad/tests/test_pandad_loopback.py b/openpilot/selfdrive/pandad/tests/test_pandad_loopback.py similarity index 97% rename from selfdrive/pandad/tests/test_pandad_loopback.py rename to openpilot/selfdrive/pandad/tests/test_pandad_loopback.py index bc90f42a83..0d0aa80046 100644 --- a/selfdrive/pandad/tests/test_pandad_loopback.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad_loopback.py @@ -6,8 +6,9 @@ import pytest from collections import defaultdict from pprint import pprint -import cereal.messaging as messaging -from cereal import car, log +import openpilot.cereal.messaging as messaging +from openpilot.cereal import log +from opendbc.car.structs import car from opendbc.car.can_definitions import CanData from openpilot.common.utils import retry from openpilot.common.params import Params diff --git a/selfdrive/pandad/tests/test_pandad_spi.py b/openpilot/selfdrive/pandad/tests/test_pandad_spi.py similarity index 85% rename from selfdrive/pandad/tests/test_pandad_spi.py rename to openpilot/selfdrive/pandad/tests/test_pandad_spi.py index 69dfb67e93..2bc302b47c 100644 --- a/selfdrive/pandad/tests/test_pandad_spi.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad_spi.py @@ -4,8 +4,9 @@ import numpy as np import pytest import random -import cereal.messaging as messaging -from cereal.services import SERVICE_LIST +import openpilot.cereal.messaging as messaging +from openpilot.cereal.services import SERVICE_LIST +from openpilot.common.timeout import Timeout from openpilot.selfdrive.test.helpers import with_processes from openpilot.selfdrive.pandad.tests.test_pandad_loopback import setup_pandad, send_random_can_messages @@ -26,16 +27,21 @@ class TestBoarddSpi: sendcan = messaging.pub_sock('sendcan') socks = {s: messaging.sub_sock(s, conflate=False, timeout=100) for s in ('can', 'pandaStates', 'peripheralState')} - time.sleep(2) - for s in socks.values(): - messaging.drain_sock_raw(s) + readiness_services = {'pandaStates', 'peripheralState'} + ready = set() + with Timeout(2, "pandad services didn't become ready"): + while not readiness_services <= ready: + for service, sock in socks.items(): + if messaging.drain_sock_raw(sock): + ready.add(service) + time.sleep(0.01) total_recv_count = 0 total_sent_count = 0 - sent_msgs = {bus: list() for bus in range(3)} + sent_msgs = {bus: [] for bus in range(3)} st = time.monotonic() - ts = {s: list() for s in socks.keys()} + ts = {s: [] for s in socks.keys()} for _ in range(int(os.getenv("TEST_TIME", "20"))): # send some CAN messages if not JUNGLE_SPAM: @@ -80,7 +86,6 @@ class TestBoarddSpi: ps = m.peripheralState assert ps.pandaType == "tres" assert 4000 < ps.voltage < 14000 - assert 50 < ps.current < 1000 assert ps.fanSpeedRpm < 10000 time.sleep(0.5) diff --git a/selfdrive/selfdrived/alertmanager.py b/openpilot/selfdrive/selfdrived/alertmanager.py similarity index 95% rename from selfdrive/selfdrived/alertmanager.py rename to openpilot/selfdrive/selfdrived/alertmanager.py index c148c62d32..3be56381cf 100644 --- a/selfdrive/selfdrived/alertmanager.py +++ b/openpilot/selfdrive/selfdrived/alertmanager.py @@ -10,7 +10,7 @@ from openpilot.selfdrive.selfdrived.events import Alert from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EmptyAlert -with open(os.path.join(BASEDIR, "selfdrive/selfdrived/alerts_offroad.json")) as f: +with open(os.path.join(BASEDIR, "openpilot/selfdrive/selfdrived/alerts_offroad.json")) as f: OFFROAD_ALERTS = json.load(f) diff --git a/selfdrive/selfdrived/alerts_offroad.json b/openpilot/selfdrive/selfdrived/alerts_offroad.json similarity index 95% rename from selfdrive/selfdrived/alerts_offroad.json rename to openpilot/selfdrive/selfdrived/alerts_offroad.json index 917b0b6ab7..c90497e8c1 100644 --- a/selfdrive/selfdrived/alerts_offroad.json +++ b/openpilot/selfdrive/selfdrived/alerts_offroad.json @@ -17,10 +17,6 @@ "severity": 1, "_comment": "Set extra field to the failed reason." }, - "Offroad_IsTakingSnapshot": { - "text": "Taking camera snapshots. System won't start until finished.", - "severity": 0 - }, "Offroad_NeosUpdate": { "text": "An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install.", "severity": 0 diff --git a/selfdrive/selfdrived/events.py b/openpilot/selfdrive/selfdrived/events.py similarity index 97% rename from selfdrive/selfdrived/events.py rename to openpilot/selfdrive/selfdrived/events.py index c9b281436c..d7eaaf70c4 100755 --- a/selfdrive/selfdrived/events.py +++ b/openpilot/selfdrive/selfdrived/events.py @@ -2,15 +2,16 @@ import math import os -from cereal import log, car -import cereal.messaging as messaging +from openpilot.cereal import log +from opendbc.car.structs import car +import openpilot.cereal.messaging as messaging from openpilot.common.constants import CV from openpilot.common.git import get_short_branch from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER from openpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ NoEntryAlert, SoftDisableAlert, UserSoftDisableAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, \ @@ -20,7 +21,7 @@ from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Pr AlertSize = log.SelfdriveState.AlertSize AlertStatus = log.SelfdriveState.AlertStatus VisualAlert = car.CarControl.HUDControl.VisualAlert -AudibleAlert = car.CarControl.HUDControl.AudibleAlert +AudibleAlert = log.SelfdriveState.AudibleAlert EventName = log.OnroadEvent.EventName @@ -96,6 +97,13 @@ def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messag Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2) +def too_distracted_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + if sm['driverMonitoringState'].lockout: + mins_left = sm['driverMonitoringState'].lockoutMinutesRemaining + return NoEntryAlert("Too Distracted", f"{mins_left} minute{'s' if mins_left != 1 else ''} Left", priority=Priority.HIGH) + return NoEntryAlert("Pay Attention to Engage", priority=Priority.HIGH) + + def audio_feedback_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: duration = FEEDBACK_MAX_DURATION - ((sm['audioFeedback'].blockNum + 1) * SAMPLE_BUFFER / SAMPLE_RATE) return NormalPermanentAlert( @@ -343,7 +351,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { "Pay Attention", "", AlertStatus.normal, AlertSize.small, - Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), + Priority.LOW, VisualAlert.none, AudibleAlert.preAlert, .1), }, EventName.driverDistracted2: { @@ -610,7 +618,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { }, EventName.tooDistracted: { - ET.NO_ENTRY: NoEntryAlert("Distraction Level Too High"), + ET.NO_ENTRY: too_distracted_alert, }, EventName.excessiveActuation: { @@ -863,7 +871,7 @@ if HARDWARE.get_device_type() == 'mici': "Pay Attention", "", AlertStatus.normal, AlertSize.small, - Priority.LOW, VisualAlert.none, AudibleAlert.none, 2), + Priority.LOW, VisualAlert.none, AudibleAlert.preAlert, 2), }, EventName.driverDistracted2: { ET.PERMANENT: Alert( @@ -926,7 +934,7 @@ if HARDWARE.get_device_type() == 'mici': if __name__ == '__main__': # print all alerts by type and priority - from cereal.services import SERVICE_LIST + from openpilot.cereal.services import SERVICE_LIST from collections import defaultdict event_names = {v: k for k, v in EventName.schema.enumerants.items()} diff --git a/selfdrive/selfdrived/helpers.py b/openpilot/selfdrive/selfdrived/helpers.py similarity index 96% rename from selfdrive/selfdrived/helpers.py rename to openpilot/selfdrive/selfdrived/helpers.py index f7468cbe43..3b3c44ecaf 100644 --- a/selfdrive/selfdrived/helpers.py +++ b/openpilot/selfdrive/selfdrived/helpers.py @@ -1,7 +1,8 @@ import math from enum import StrEnum, auto -from cereal import car, messaging +from openpilot.cereal import messaging +from opendbc.car.structs import car from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.locationd.helpers import Pose from opendbc.car import ACCELERATION_DUE_TO_GRAVITY diff --git a/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py similarity index 97% rename from selfdrive/selfdrived/selfdrived.py rename to openpilot/selfdrive/selfdrived/selfdrived.py index 30605d0a27..2edc0bce18 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -3,9 +3,10 @@ import os import time import threading -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging -from cereal import car, log, custom +from openpilot.cereal import log, custom +from opendbc.car.structs import car from msgq.visionipc import VisionIpcClient, VisionStreamType @@ -21,8 +22,8 @@ from openpilot.selfdrive.selfdrived.helpers import ExcessiveActuationCheck from openpilot.selfdrive.selfdrived.state import StateMachine from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroad_alert -from openpilot.system.version import get_build_metadata -from openpilot.system.hardware import HARDWARE +from openpilot.common.version import get_build_metadata +from openpilot.common.hardware import HARDWARE from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem from openpilot.sunnypilot import get_sanitize_int_param @@ -222,10 +223,13 @@ class SelfdriveD(CruiseHelper): # Handle DM if not self.CP.notCar: - # Block engaging until ignition cycle after max number or time of distractions + # Block engaging until lockout times out or ignition reset if self.sm['driverMonitoringState'].lockout and not self.dm_lockout_set: self.params.put_bool("DriverTooDistracted", True) self.dm_lockout_set = True + elif not self.sm['driverMonitoringState'].lockout and self.dm_lockout_set: + self.params.remove("DriverTooDistracted") + self.dm_lockout_set = False # No entry conditions if self.sm['driverMonitoringState'].lockout or self.sm['driverMonitoringState'].alwaysOnLockout: self.events.add(EventName.tooDistracted) @@ -376,12 +380,13 @@ class SelfdriveD(CruiseHelper): self.events.add(EventName.cameraFrameRate) if not REPLAY and self.rk.lagging: self.events.add(EventName.selfdrivedLagging) - if self.sm['radarState'].radarErrors.canError: - self.events.add(EventName.canError) - elif self.sm['radarState'].radarErrors.radarUnavailableTemporary: - self.events.add(EventName.radarTempUnavailable) - elif any(self.sm['radarState'].radarErrors.to_dict().values()): - self.events.add(EventName.radarFault) + if self.CP.openpilotLongitudinalControl: + if self.sm['radarState'].radarErrors.canError: + self.events.add(EventName.canError) + elif self.sm['radarState'].radarErrors.radarUnavailableTemporary: + self.events.add(EventName.radarTempUnavailable) + elif any(self.sm['radarState'].radarErrors.to_dict().values()): + self.events.add(EventName.radarFault) if not self.sm.valid['pandaStates']: self.events.add(EventName.usbError) if CS.canTimeout: diff --git a/selfdrive/selfdrived/state.py b/openpilot/selfdrive/selfdrived/state.py similarity index 96% rename from selfdrive/selfdrived/state.py rename to openpilot/selfdrive/selfdrived/state.py index 073ddb56eb..eca0019503 100644 --- a/selfdrive/selfdrived/state.py +++ b/openpilot/selfdrive/selfdrived/state.py @@ -1,4 +1,4 @@ -from cereal import log +from openpilot.cereal import log from openpilot.selfdrive.selfdrived.events import Events, ET from openpilot.common.realtime import DT_CTRL @@ -24,14 +24,14 @@ class StateMachine: # ENABLED, SOFT DISABLING, PRE ENABLING, OVERRIDING if self.state != State.disabled: # user and immediate disable always have priority in a non-disabled state - if events.contains(ET.USER_DISABLE): - self.state = State.disabled - self.current_alert_types.append(ET.USER_DISABLE) - - elif events.contains(ET.IMMEDIATE_DISABLE): + if events.contains(ET.IMMEDIATE_DISABLE): self.state = State.disabled self.current_alert_types.append(ET.IMMEDIATE_DISABLE) + elif events.contains(ET.USER_DISABLE): + self.state = State.disabled + self.current_alert_types.append(ET.USER_DISABLE) + else: # ENABLED if self.state == State.enabled: diff --git a/selfdrive/selfdrived/tests/test_alertmanager.py b/openpilot/selfdrive/selfdrived/tests/test_alertmanager.py similarity index 100% rename from selfdrive/selfdrived/tests/test_alertmanager.py rename to openpilot/selfdrive/selfdrived/tests/test_alertmanager.py diff --git a/selfdrive/selfdrived/tests/test_alerts.py b/openpilot/selfdrive/selfdrived/tests/test_alerts.py similarity index 93% rename from selfdrive/selfdrived/tests/test_alerts.py rename to openpilot/selfdrive/selfdrived/tests/test_alerts.py index 4cad9990be..0210c138da 100644 --- a/selfdrive/selfdrived/tests/test_alerts.py +++ b/openpilot/selfdrive/selfdrived/tests/test_alerts.py @@ -4,8 +4,9 @@ import os import random from PIL import Image, ImageDraw, ImageFont -from cereal import log, car -from cereal.messaging import SubMaster +from openpilot.cereal import log +from opendbc.car.structs import car +from openpilot.cereal.messaging import SubMaster from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.selfdrive.selfdrived.events import Alert, EVENTS, ET @@ -14,7 +15,7 @@ from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS AlertSize = log.SelfdriveState.AlertSize -OFFROAD_ALERTS_PATH = os.path.join(BASEDIR, "selfdrive/selfdrived/alerts_offroad.json") +OFFROAD_ALERTS_PATH = os.path.join(BASEDIR, "openpilot/selfdrive/selfdrived/alerts_offroad.json") # TODO: add callback alerts ALERTS = [] @@ -47,7 +48,7 @@ class TestAlerts: # ensure alert text doesn't exceed allowed width def test_alert_text_length(self): - font_path = os.path.join(BASEDIR, "selfdrive/assets/fonts") + font_path = os.path.join(BASEDIR, "openpilot/selfdrive/assets/fonts") regular_font_path = os.path.join(font_path, "Inter-SemiBold.ttf") bold_font_path = os.path.join(font_path, "Inter-Bold.ttf") semibold_font_path = os.path.join(font_path, "Inter-SemiBold.ttf") diff --git a/selfdrive/selfdrived/tests/test_state_machine.py b/openpilot/selfdrive/selfdrived/tests/test_state_machine.py similarity index 95% rename from selfdrive/selfdrived/tests/test_state_machine.py rename to openpilot/selfdrive/selfdrived/tests/test_state_machine.py index b720f48f1e..ec8f068039 100644 --- a/selfdrive/selfdrived/tests/test_state_machine.py +++ b/openpilot/selfdrive/selfdrived/tests/test_state_machine.py @@ -1,4 +1,4 @@ -from cereal import log +from openpilot.cereal import log from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.selfdrived.state import StateMachine, SOFT_DISABLE_TIME from openpilot.selfdrive.selfdrived.events import Events, ET, EVENTS, NormalPermanentAlert @@ -13,11 +13,11 @@ ALL_STATES = tuple(State.schema.enumerants.values()) ENABLE_EVENT_TYPES = (ET.ENABLE, ET.PRE_ENABLE, ET.OVERRIDE_LATERAL, ET.OVERRIDE_LONGITUDINAL) -def make_event(event_types): - event = {} +def make_event(event_types: list[str | None]): + EVENTS[0] = {} for ev in event_types: - event[ev] = NormalPermanentAlert("alert") - EVENTS[0] = event + if ev is not None: + EVENTS[0][ev] = NormalPermanentAlert("alert") return 0 diff --git a/selfdrive/test/.gitignore b/openpilot/selfdrive/test/.gitignore similarity index 90% rename from selfdrive/test/.gitignore rename to openpilot/selfdrive/test/.gitignore index b8c6bebd95..377696afae 100644 --- a/selfdrive/test/.gitignore +++ b/openpilot/selfdrive/test/.gitignore @@ -1,5 +1,4 @@ out/ -docker_out/ process_replay/diff.txt process_replay/model_diff.txt diff --git a/selfdrive/pandad/tests/__init__.py b/openpilot/selfdrive/test/__init__.py similarity index 100% rename from selfdrive/pandad/tests/__init__.py rename to openpilot/selfdrive/test/__init__.py diff --git a/selfdrive/test/cpp_harness.py b/openpilot/selfdrive/test/cpp_harness.py similarity index 100% rename from selfdrive/test/cpp_harness.py rename to openpilot/selfdrive/test/cpp_harness.py diff --git a/selfdrive/test/fuzzy_generation.py b/openpilot/selfdrive/test/fuzzy_generation.py similarity index 99% rename from selfdrive/test/fuzzy_generation.py rename to openpilot/selfdrive/test/fuzzy_generation.py index 9f028a8fc8..c97221ae9e 100644 --- a/selfdrive/test/fuzzy_generation.py +++ b/openpilot/selfdrive/test/fuzzy_generation.py @@ -4,7 +4,7 @@ from typing import Any from collections.abc import Callable from functools import cache -from cereal import log +from openpilot.cereal import log DrawType = Callable[[st.SearchStrategy], Any] diff --git a/selfdrive/test/helpers.py b/openpilot/selfdrive/test/helpers.py similarity index 97% rename from selfdrive/test/helpers.py rename to openpilot/selfdrive/test/helpers.py index 49a8ec60a3..9d954216b8 100644 --- a/selfdrive/test/helpers.py +++ b/openpilot/selfdrive/test/helpers.py @@ -7,10 +7,10 @@ import pytest from functools import wraps -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.common.params import Params from openpilot.system.manager.process_config import managed_processes -from openpilot.system.version import training_version, terms_version +from openpilot.common.version import training_version, terms_version def set_params_enabled(): diff --git a/selfdrive/test/longitudinal_maneuvers/.gitignore b/openpilot/selfdrive/test/longitudinal_maneuvers/.gitignore similarity index 100% rename from selfdrive/test/longitudinal_maneuvers/.gitignore rename to openpilot/selfdrive/test/longitudinal_maneuvers/.gitignore diff --git a/selfdrive/test/__init__.py b/openpilot/selfdrive/test/longitudinal_maneuvers/__init__.py similarity index 100% rename from selfdrive/test/__init__.py rename to openpilot/selfdrive/test/longitudinal_maneuvers/__init__.py diff --git a/selfdrive/test/longitudinal_maneuvers/maneuver.py b/openpilot/selfdrive/test/longitudinal_maneuvers/maneuver.py similarity index 100% rename from selfdrive/test/longitudinal_maneuvers/maneuver.py rename to openpilot/selfdrive/test/longitudinal_maneuvers/maneuver.py diff --git a/selfdrive/test/longitudinal_maneuvers/plant.py b/openpilot/selfdrive/test/longitudinal_maneuvers/plant.py similarity index 98% rename from selfdrive/test/longitudinal_maneuvers/plant.py rename to openpilot/selfdrive/test/longitudinal_maneuvers/plant.py index 1431e60019..abbd6be1d6 100755 --- a/selfdrive/test/longitudinal_maneuvers/plant.py +++ b/openpilot/selfdrive/test/longitudinal_maneuvers/plant.py @@ -2,8 +2,8 @@ import time import numpy as np -from cereal import log -import cereal.messaging as messaging +from openpilot.cereal import log +import openpilot.cereal.messaging as messaging from openpilot.common.realtime import Ratekeeper, DT_MDL from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState from openpilot.selfdrive.modeld.constants import ModelConstants @@ -94,14 +94,14 @@ class Plant: lead.dRel = float(d_rel) lead.yRel = 0.0 lead.vRel = float(v_rel) - lead.aRel = float(a_lead - self.acceleration) lead.vLead = float(v_lead) lead.vLeadK = float(v_lead) lead.aLeadK = float(a_lead) # TODO use real radard logic for this lead.aLeadTau = float(_LEAD_ACCEL_TAU) - lead.status = status + lead.present = status lead.modelProb = float(prob_lead) + lead.radar = True if not self.only_lead2: radar.radarState.leadOne = lead radar.radarState.leadTwo = lead diff --git a/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py b/openpilot/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py similarity index 100% rename from selfdrive/test/longitudinal_maneuvers/test_longitudinal.py rename to openpilot/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py diff --git a/selfdrive/debug/mem_usage.py b/openpilot/selfdrive/test/mem_usage.py old mode 100755 new mode 100644 similarity index 89% rename from selfdrive/debug/mem_usage.py rename to openpilot/selfdrive/test/mem_usage.py index bc0e97e7ca..02b7934466 --- a/selfdrive/debug/mem_usage.py +++ b/openpilot/selfdrive/test/mem_usage.py @@ -1,16 +1,13 @@ -#!/usr/bin/env python3 -import argparse import os from collections import defaultdict import numpy as np from openpilot.common.utils import tabulate -from openpilot.tools.lib.logreader import LogReader DEMO_ROUTE = "5beb9b58bd12b691/0000010a--a51155e496" MB = 1024 * 1024 -TABULATE_OPTS = dict(tablefmt="simple_grid", stralign="center", numalign="center") +TABULATE_OPTS = {"tablefmt": "simple_grid", "stralign": "center", "numalign": "center"} def _get_procs(): @@ -210,29 +207,3 @@ def print_report(proc_logs, device_states=None): 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/test/process_replay/README.md b/openpilot/selfdrive/test/process_replay/README.md similarity index 100% rename from selfdrive/test/process_replay/README.md rename to openpilot/selfdrive/test/process_replay/README.md diff --git a/selfdrive/test/process_replay/__init__.py b/openpilot/selfdrive/test/process_replay/__init__.py similarity index 100% rename from selfdrive/test/process_replay/__init__.py rename to openpilot/selfdrive/test/process_replay/__init__.py diff --git a/selfdrive/test/process_replay/capture.py b/openpilot/selfdrive/test/process_replay/capture.py similarity index 100% rename from selfdrive/test/process_replay/capture.py rename to openpilot/selfdrive/test/process_replay/capture.py diff --git a/selfdrive/test/process_replay/compare_logs.py b/openpilot/selfdrive/test/process_replay/compare_logs.py similarity index 100% rename from selfdrive/test/process_replay/compare_logs.py rename to openpilot/selfdrive/test/process_replay/compare_logs.py diff --git a/selfdrive/test/process_replay/diff_report.py b/openpilot/selfdrive/test/process_replay/diff_report.py similarity index 99% rename from selfdrive/test/process_replay/diff_report.py rename to openpilot/selfdrive/test/process_replay/diff_report.py index 5da78657f4..c6c1f01abd 100644 --- a/selfdrive/test/process_replay/diff_report.py +++ b/openpilot/selfdrive/test/process_replay/diff_report.py @@ -1,6 +1,5 @@ import os from collections import defaultdict - from opendbc.car.tests.car_diff import format_diff, format_numeric_diffs from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs from openpilot.selfdrive.test.process_replay.process_replay import PROC_REPLAY_DIR diff --git a/selfdrive/test/process_replay/migration.py b/openpilot/selfdrive/test/process_replay/migration.py similarity index 98% rename from selfdrive/test/process_replay/migration.py rename to openpilot/selfdrive/test/process_replay/migration.py index 09fdb40b67..9b0fd2b91f 100644 --- a/selfdrive/test/process_replay/migration.py +++ b/openpilot/selfdrive/test/process_replay/migration.py @@ -5,7 +5,8 @@ import capnp import functools import traceback -from cereal import messaging, car, log +from openpilot.cereal import messaging, log +from opendbc.car.structs import car from opendbc.car.fingerprints import MIGRATION from opendbc.car.toyota.values import EPS_SCALE, ToyotaSafetyFlags from opendbc.car.ford.values import CAR as FORD, FordFlags, FordSafetyFlags @@ -176,8 +177,6 @@ def migrate_liveTracks(msgs): pt.dRel = track.dRel pt.yRel = track.yRel pt.vRel = track.vRel - pt.aRel = track.aRel - pt.measured = True pts.append(pt) new_msg.liveTracks.points = pts @@ -298,7 +297,7 @@ def migrate_carOutput(msgs): co = messaging.new_message('carOutput') co.valid = msg.valid co.logMonoTime = msg.logMonoTime - co.carOutput.actuatorsOutput = msg.carControl.actuatorsOutputDEPRECATED + co.carOutput.actuatorsOutput = msg.carControl.deprecated.actuatorsOutput add_ops.append(as_reader(co)) return [], add_ops, [] @@ -324,10 +323,10 @@ def migrate_pandaStates(msgs): safety_param = safety_param_migration[fingerprint].value elif len(CP.safetyConfigs): safety_param = CP.safetyConfigs[0].safetyParam - if CP.safetyConfigs[0].safetyParamDEPRECATED != 0: - safety_param = CP.safetyConfigs[0].safetyParamDEPRECATED + if CP.safetyConfigs[0].deprecated.safetyParam != 0: + safety_param = CP.safetyConfigs[0].deprecated.safetyParam else: - safety_param = CP.safetyParamDEPRECATED + safety_param = CP.deprecated.safetyParam ops = [] for index, msg in msgs: diff --git a/selfdrive/test/process_replay/model_replay.py b/openpilot/selfdrive/test/process_replay/model_replay.py similarity index 99% rename from selfdrive/test/process_replay/model_replay.py rename to openpilot/selfdrive/test/process_replay/model_replay.py index 266f66aaf2..3e5616e702 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/openpilot/selfdrive/test/process_replay/model_replay.py @@ -12,7 +12,7 @@ import numpy as np from openpilot.common.utils import tabulate from openpilot.common.git import get_commit -from openpilot.system.hardware import PC +from openpilot.common.hardware import PC 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 get_process_config, replay_process diff --git a/selfdrive/test/process_replay/process_replay.py b/openpilot/selfdrive/test/process_replay/process_replay.py similarity index 99% rename from selfdrive/test/process_replay/process_replay.py rename to openpilot/selfdrive/test/process_replay/process_replay.py index 77254cecee..fc3463b376 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/openpilot/selfdrive/test/process_replay/process_replay.py @@ -12,11 +12,11 @@ from typing import Any from collections.abc import Callable, Iterable from tqdm import tqdm import capnp -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths -import cereal.messaging as messaging -from cereal import car -from cereal.services import SERVICE_LIST +import openpilot.cereal.messaging as messaging +from opendbc.car.structs import car +from openpilot.cereal.services import SERVICE_LIST from msgq.visionipc import VisionIpcServer, get_endpoint_name as vipc_get_endpoint_name from opendbc.car.can_definitions import CanData from opendbc.car.car_helpers import get_car, interfaces @@ -216,8 +216,7 @@ class ProcessContainer: def _start_process(self): if self.capture is not None: - self.process.launcher = LauncherWithCapture(self.capture, self.process.launcher) - self.process.prepare() + self.process.launcher = LauncherWithCapture(self.capture, self.process.launcher) # ty: ignore[invalid-assignment] # intentional wrapper self.process.start() def start( @@ -635,10 +634,10 @@ def replay_process( fingerprint: str | None = None, return_all_logs: bool = False, custom_params: dict[str, Any] | None = None, captured_output_store: dict[str, dict[str, str]] | None = None, disable_progress: bool = False ) -> list[capnp._DynamicStructReader]: - if isinstance(cfg, Iterable): - cfgs = list(cfg) - else: + if isinstance(cfg, ProcessConfig): cfgs = [cfg] + else: + cfgs = list(cfg) all_msgs = migrate_all(lr, manager_states=True, diff --git a/selfdrive/test/process_replay/regen.py b/openpilot/selfdrive/test/process_replay/regen.py similarity index 100% rename from selfdrive/test/process_replay/regen.py rename to openpilot/selfdrive/test/process_replay/regen.py diff --git a/selfdrive/test/process_replay/regen_all.py b/openpilot/selfdrive/test/process_replay/regen_all.py similarity index 100% rename from selfdrive/test/process_replay/regen_all.py rename to openpilot/selfdrive/test/process_replay/regen_all.py diff --git a/selfdrive/test/process_replay/test_fuzzy.py b/openpilot/selfdrive/test/process_replay/test_fuzzy.py similarity index 97% rename from selfdrive/test/process_replay/test_fuzzy.py rename to openpilot/selfdrive/test/process_replay/test_fuzzy.py index 6989f8957f..372b368608 100644 --- a/selfdrive/test/process_replay/test_fuzzy.py +++ b/openpilot/selfdrive/test/process_replay/test_fuzzy.py @@ -4,7 +4,7 @@ from hypothesis import given, HealthCheck, Phase, settings import hypothesis.strategies as st from openpilot.common.parameterized import parameterized -from cereal import log +from openpilot.cereal import log from opendbc.car.toyota.values import CAR as TOYOTA from openpilot.selfdrive.test.fuzzy_generation import FuzzyGenerator import openpilot.selfdrive.test.process_replay.process_replay as pr diff --git a/selfdrive/test/process_replay/test_processes.py b/openpilot/selfdrive/test/process_replay/test_processes.py similarity index 99% rename from selfdrive/test/process_replay/test_processes.py rename to openpilot/selfdrive/test/process_replay/test_processes.py index bc0085534c..d9d827add5 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/openpilot/selfdrive/test/process_replay/test_processes.py @@ -7,7 +7,6 @@ import traceback from collections import defaultdict from tqdm import tqdm 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 diff --git a/selfdrive/test/process_replay/test_regen.py b/openpilot/selfdrive/test/process_replay/test_regen.py similarity index 100% rename from selfdrive/test/process_replay/test_regen.py rename to openpilot/selfdrive/test/process_replay/test_regen.py diff --git a/selfdrive/test/process_replay/vision_meta.py b/openpilot/selfdrive/test/process_replay/vision_meta.py similarity index 100% rename from selfdrive/test/process_replay/vision_meta.py rename to openpilot/selfdrive/test/process_replay/vision_meta.py diff --git a/selfdrive/test/scons_build_test.sh b/openpilot/selfdrive/test/scons_build_test.sh similarity index 89% rename from selfdrive/test/scons_build_test.sh rename to openpilot/selfdrive/test/scons_build_test.sh index d4a0733569..6f6eafcad1 100755 --- a/selfdrive/test/scons_build_test.sh +++ b/openpilot/selfdrive/test/scons_build_test.sh @@ -2,7 +2,7 @@ set -e SCRIPT_DIR=$(dirname "$0") -BASEDIR=$(realpath "$SCRIPT_DIR/../../") +BASEDIR=$(realpath "$SCRIPT_DIR/../../../") cd $BASEDIR # tests that our build system's dependencies are configured properly, diff --git a/selfdrive/test/setup_device_ci.sh b/openpilot/selfdrive/test/setup_device_ci.sh similarity index 70% rename from selfdrive/test/setup_device_ci.sh rename to openpilot/selfdrive/test/setup_device_ci.sh index 3fe353e72c..60631ce037 100755 --- a/selfdrive/test/setup_device_ci.sh +++ b/openpilot/selfdrive/test/setup_device_ci.sh @@ -3,6 +3,7 @@ set -e set -x + if [ -z "$SOURCE_DIR" ]; then echo "SOURCE_DIR must be set" exit 1 @@ -55,6 +56,28 @@ sleep infinity EOF chmod +x $CONTINUE_PATH +export GIT_LFS_SKIP_SMUDGE=1 +pull_lfs() { + # The big driving model is not used on these devices yet. Keep its pointer in + # the worktree, but don't download or copy the 1.8 GB LFS object. + LFS_EXCLUDE="openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" + + git config --local lfs.fetchexclude "$LFS_EXCLUDE" + git lfs pull --exclude="$LFS_EXCLUDE" + if git cat-file -e "HEAD:$LFS_EXCLUDE"; then + rm -f "$LFS_EXCLUDE" + git checkout -- "$LFS_EXCLUDE" + + # `git lfs prune` retains objects referenced by HEAD, even when excluded. + # Remove this one explicitly so safe checkout doesn't rsync it either. + oid=$(git show "HEAD:$LFS_EXCLUDE" | sed -n 's/^oid sha256://p') + lfs_objects=$(git lfs env | sed -n 's/^LocalMediaDir=//p') + if [[ "$oid" =~ ^[0-9a-f]{64}$ && -n "$lfs_objects" ]]; then + rm -f "$lfs_objects/${oid:0:2}/${oid:2:2}/$oid" + fi + fi +} + safe_checkout() { # completely clean TEST_DIR @@ -74,8 +97,7 @@ safe_checkout() { git submodule update --init --recursive git submodule foreach --recursive "git reset --hard && git clean -xdff" - git lfs pull - (ulimit -n 65535 && git lfs prune) + pull_lfs echo "git checkout done, t=$SECONDS" du -hs $SOURCE_DIR $SOURCE_DIR/.git @@ -100,8 +122,7 @@ unsafe_checkout() {( set -e git submodule update --init --recursive git submodule foreach --recursive "git reset --hard && git clean -df" - git lfs pull - (ulimit -n 65535 && git lfs prune) + pull_lfs )} export GIT_PACK_THREADS=8 @@ -124,4 +145,12 @@ else safe_checkout fi +# submodule package symlinks for PYTHONPATH imports on device (same as launch_chffrplus.sh) +cd $TEST_DIR +ln -sfn msgq_repo/msgq msgq +ln -sfn opendbc_repo/opendbc opendbc +ln -sfn rednose_repo/rednose rednose +ln -sfn teleoprtc_repo/teleoprtc teleoprtc +ln -sfn tinygrad_repo/tinygrad tinygrad + echo "$TEST_DIR synced with $GIT_COMMIT, t=$SECONDS" diff --git a/selfdrive/test/test_onroad.py b/openpilot/selfdrive/test/test_onroad.py similarity index 90% rename from selfdrive/test/test_onroad.py rename to openpilot/selfdrive/test/test_onroad.py index 8161dca130..b15d890452 100644 --- a/selfdrive/test/test_onroad.py +++ b/openpilot/selfdrive/test/test_onroad.py @@ -10,15 +10,15 @@ from collections import Counter, defaultdict from pathlib import Path from openpilot.common.utils import tabulate -from cereal import log -import cereal.messaging as messaging -from cereal.services import SERVICE_LIST +from openpilot.cereal import log +import openpilot.cereal.messaging as messaging +from openpilot.cereal.services import SERVICE_LIST from openpilot.common.basedir import BASEDIR from openpilot.common.timeout import Timeout from openpilot.common.params import Params from openpilot.selfdrive.selfdrived.events import EVENTS, ET from openpilot.selfdrive.test.helpers import set_params_enabled, release_only -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.tools.lib.logreader import LogReader from openpilot.tools.lib.log_time_series import msgs_to_time_series @@ -35,40 +35,39 @@ LOG_OFFSET = 8 MAX_TOTAL_CPU = 350. # total for all 8 cores PROCS = { # Baseline CPU usage by process - "selfdrive.controls.controlsd": 16.0, - "selfdrive.selfdrived.selfdrived": 16.0, - "selfdrive.car.card": 26.0, + "openpilot.selfdrive.controls.controlsd": 16.0, + "openpilot.selfdrive.selfdrived.selfdrived": 16.0, + "openpilot.selfdrive.car.card": 26.0, "./loggerd": 14.0, "./encoderd": 13.0, "./camerad": 10.0, - "selfdrive.controls.plannerd": 8.0, - "selfdrive.ui.ui": 40.0, - "system.sensord.sensord": 13.0, - "selfdrive.controls.radard": 2.0, - "selfdrive.modeld.modeld": 22.0, - "selfdrive.modeld.dmonitoringmodeld": 18.0, - "system.hardware.hardwared": 4.0, - "selfdrive.locationd.calibrationd": 2.0, - "selfdrive.locationd.torqued": 5.0, - "selfdrive.locationd.locationd": 25.0, - "selfdrive.locationd.paramsd": 9.0, - "selfdrive.locationd.lagd": 11.0, - "selfdrive.ui.soundd": 3.0, - "selfdrive.ui.feedback.feedbackd": 1.0, - "selfdrive.monitoring.dmonitoringd": 4.0, - "system.proclogd": 7.0, - "system.logmessaged": 1.0, - "system.tombstoned": 0, - "system.journald": 1.0, - "system.micd": 5.0, - "system.timed": 0, - "selfdrive.pandad.pandad": 0, - "system.statsd": 1.0, - "system.loggerd.uploader": 15.0, - "system.loggerd.deleter": 1.0, + "openpilot.selfdrive.controls.plannerd": 8.0, + "openpilot.selfdrive.ui.ui": 40.0, + "openpilot.system.sensord.sensord": 13.0, + "openpilot.selfdrive.controls.radard": 2.0, + "openpilot.selfdrive.modeld.modeld": 22.0, + "openpilot.selfdrive.modeld.dmonitoringmodeld": 18.0, + "openpilot.system.hardware.hardwared": 4.0, + "openpilot.selfdrive.locationd.calibrationd": 2.0, + "openpilot.selfdrive.locationd.torqued": 5.0, + "openpilot.selfdrive.locationd.locationd": 25.0, + "openpilot.selfdrive.locationd.paramsd": 9.0, + "openpilot.selfdrive.locationd.lagd": 11.0, + "openpilot.selfdrive.ui.soundd": 3.0, + "openpilot.selfdrive.ui.feedback.feedbackd": 1.0, + "openpilot.selfdrive.monitoring.dmonitoringd": 4.0, + "openpilot.system.proclogd": 7.0, + "openpilot.system.logmessaged": 1.0, + "openpilot.system.tombstoned": 0, + "openpilot.system.journald": 1.0, + "openpilot.system.micd": 5.0, + "openpilot.system.timed": 0, + "openpilot.selfdrive.pandad.pandad": 0, + "openpilot.system.loggerd.uploader": 15.0, + "openpilot.system.loggerd.deleter": 1.0, "./pandad": 19.0, - "system.qcomgpsd.qcomgpsd": 1.0, - "system.hardware.tici.modem": 10.0, + "openpilot.system.qcomgpsd.qcomgpsd": 1.0, + "openpilot.common.hardware.tici.modem": 10.0, } TIMINGS = { @@ -130,7 +129,7 @@ class TestOnroad: # start manager and run openpilot for TEST_DURATION proc = None try: - manager_path = os.path.join(BASEDIR, "system/manager/manager.py") + manager_path = os.path.join(BASEDIR, "openpilot/system/manager/manager.py") cls.manager_st = time.monotonic() proc = subprocess.Popen(["python", manager_path]) @@ -176,8 +175,9 @@ class TestOnroad: if s in ('ubloxGnss', 'ubloxRaw', 'gnssMeasurements', 'gpsLocation', 'gpsLocationExternal', 'qcomGnss'): continue + duration = TEST_DURATION - 5.0 # subtract some selfdrived initializing time with subtests.test(service=s): - assert len(msgs) >= math.floor(SERVICE_LIST[s].frequency*int(TEST_DURATION*0.8)) + assert len(msgs) >= math.floor(SERVICE_LIST[s].frequency*int(duration*0.8)) def test_manager_starting_time(self): st = self.ts['managerState']['t'][0] @@ -284,7 +284,7 @@ class TestOnroad: print("--------------- Memory Usage -------------------") print("------------------------------------------------") - from openpilot.selfdrive.debug.mem_usage import print_report + from openpilot.selfdrive.test.mem_usage import print_report print_report(self.msgs['procLog'], self.msgs['deviceState']) offset = int(SERVICE_LIST['deviceState'].frequency * LOG_OFFSET) diff --git a/system/hardware/tici/tests/test_power_draw.py b/openpilot/selfdrive/test/test_power_draw.py similarity index 94% rename from system/hardware/tici/tests/test_power_draw.py rename to openpilot/selfdrive/test/test_power_draw.py index 6656addf73..9bc012ceaa 100644 --- a/system/hardware/tici/tests/test_power_draw.py +++ b/openpilot/selfdrive/test/test_power_draw.py @@ -5,16 +5,16 @@ import numpy as np from dataclasses import dataclass from openpilot.common.utils import tabulate -import cereal.messaging as messaging -from cereal.services import SERVICE_LIST +import openpilot.cereal.messaging as messaging +from openpilot.cereal.services import SERVICE_LIST from opendbc.car.car_helpers import get_demo_car_params from openpilot.common.mock import mock_messages from openpilot.common.params import Params -from openpilot.system.hardware.tici.power_monitor import get_power +from openpilot.common.hardware.tici.power_monitor import get_power from openpilot.system.manager.process_config import managed_processes from openpilot.system.manager.manager import manager_cleanup -SAMPLE_TIME = 8 # seconds to sample power +SAMPLE_TIME = 2 # seconds to sample power MAX_WARMUP_TIME = 30 # seconds to wait for SAMPLE_TIME consecutive valid samples @dataclass @@ -44,9 +44,6 @@ class TestPowerDraw: def setup_method(self): Params().put("CarParams", get_demo_car_params().to_bytes(), block=True) - # wait a bit for power save to disable - time.sleep(5) - def teardown_method(self): manager_cleanup() diff --git a/selfdrive/test/update_ci_routes.py b/openpilot/selfdrive/test/update_ci_routes.py similarity index 99% rename from selfdrive/test/update_ci_routes.py rename to openpilot/selfdrive/test/update_ci_routes.py index 54e1c88718..40f74ee93c 100755 --- a/selfdrive/test/update_ci_routes.py +++ b/openpilot/selfdrive/test/update_ci_routes.py @@ -6,7 +6,6 @@ import sys from collections.abc import Iterable from tqdm import tqdm - from opendbc.car.tests.routes import routes as test_car_models_routes from openpilot.selfdrive.test.process_replay.test_processes import source_segments as replay_segments from openpilot.tools.lib.azure_container import AzureContainer diff --git a/selfdrive/ui/.gitignore b/openpilot/selfdrive/ui/.gitignore similarity index 100% rename from selfdrive/ui/.gitignore rename to openpilot/selfdrive/ui/.gitignore diff --git a/selfdrive/ui/SConscript b/openpilot/selfdrive/ui/SConscript similarity index 92% rename from selfdrive/ui/SConscript rename to openpilot/selfdrive/ui/SConscript index 3d668aef8f..d187b4ca3c 100644 --- a/selfdrive/ui/SConscript +++ b/openpilot/selfdrive/ui/SConscript @@ -4,8 +4,8 @@ import importlib.util Import('env', 'arch', 'common') # build the fonts -generator = File("#selfdrive/assets/fonts/process.py") -source_files = Glob("#selfdrive/assets/fonts/*.ttf") + Glob("#selfdrive/assets/fonts/*.otf") +generator = File("#openpilot/selfdrive/assets/fonts/process.py") +source_files = Glob("#openpilot/selfdrive/assets/fonts/*.ttf") + Glob("#openpilot/selfdrive/assets/fonts/*.otf") output_files = [ (f"#{Path(f.path).with_suffix('.fnt')}", f"#{Path(f.path).with_suffix('.png')}") for f in source_files diff --git a/selfdrive/ui/__init__.py b/openpilot/selfdrive/ui/__init__.py similarity index 100% rename from selfdrive/ui/__init__.py rename to openpilot/selfdrive/ui/__init__.py diff --git a/selfdrive/test/longitudinal_maneuvers/__init__.py b/openpilot/selfdrive/ui/body/__init__.py similarity index 100% rename from selfdrive/test/longitudinal_maneuvers/__init__.py rename to openpilot/selfdrive/ui/body/__init__.py diff --git a/selfdrive/ui/body/animations.py b/openpilot/selfdrive/ui/body/animations.py similarity index 96% rename from selfdrive/ui/body/animations.py rename to openpilot/selfdrive/ui/body/animations.py index c40f7ecdef..302f8989bf 100644 --- a/selfdrive/ui/body/animations.py +++ b/openpilot/selfdrive/ui/body/animations.py @@ -204,7 +204,10 @@ class FaceAnimator: frames_back = round(rewind_elapsed / self._animation.frame_duration) frame_index = self._rewind_from - frames_back if frame_index <= 0: - return self._switch_to_next(now) + if self._next is None: + self._rewinding = False + return self._animation.frames[0] + return self._switch_to_next(now, self._next) return self._animation.frames[frame_index] # Play starting frames first (once) @@ -223,7 +226,7 @@ class FaceAnimator: if self._next is not None: if frame_index == 0 and (len(self._animation.frames) == 1 or self._seen_nonzero): - return self._switch_to_next(now) + return self._switch_to_next(now, self._next) # No natural return to frame 0 — start rewinding if self._animation.mode in (AnimationMode.ONCE_FORWARD, AnimationMode.REPEAT_FORWARD): self._rewinding = True @@ -232,8 +235,8 @@ class FaceAnimator: return self._animation.frames[frame_index] - def _switch_to_next(self, now: float) -> list[tuple[int, int]]: - self._animation = self._next + def _switch_to_next(self, now: float, animation: Animation) -> list[tuple[int, int]]: + self._animation = animation self._next = None self._rewinding = False self._seen_nonzero = False diff --git a/selfdrive/ui/body/__init__.py b/openpilot/selfdrive/ui/body/layouts/__init__.py similarity index 100% rename from selfdrive/ui/body/__init__.py rename to openpilot/selfdrive/ui/body/layouts/__init__.py diff --git a/selfdrive/ui/body/layouts/onroad.py b/openpilot/selfdrive/ui/body/layouts/onroad.py similarity index 100% rename from selfdrive/ui/body/layouts/onroad.py rename to openpilot/selfdrive/ui/body/layouts/onroad.py diff --git a/selfdrive/ui/feedback/feedbackd.py b/openpilot/selfdrive/ui/feedback/feedbackd.py similarity index 97% rename from selfdrive/ui/feedback/feedbackd.py rename to openpilot/selfdrive/ui/feedback/feedbackd.py index 24f27874eb..8056a580c6 100755 --- a/selfdrive/ui/feedback/feedbackd.py +++ b/openpilot/selfdrive/ui/feedback/feedbackd.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog -from cereal import car +from opendbc.car.structs import car from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER FEEDBACK_MAX_DURATION = 10.0 diff --git a/selfdrive/ui/installer/continue_openpilot.sh b/openpilot/selfdrive/ui/installer/continue_openpilot.sh similarity index 100% rename from selfdrive/ui/installer/continue_openpilot.sh rename to openpilot/selfdrive/ui/installer/continue_openpilot.sh diff --git a/selfdrive/ui/installer/installer.cc b/openpilot/selfdrive/ui/installer/installer.cc similarity index 99% rename from selfdrive/ui/installer/installer.cc rename to openpilot/selfdrive/ui/installer/installer.cc index 176c2d510f..07e824c649 100644 --- a/selfdrive/ui/installer/installer.cc +++ b/openpilot/selfdrive/ui/installer/installer.cc @@ -5,7 +5,7 @@ #include "common/swaglog.h" #include "common/util.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include "raylib.h" int freshClone(); diff --git a/selfdrive/ui/installer/inter-ascii.ttf b/openpilot/selfdrive/ui/installer/inter-ascii.ttf similarity index 100% rename from selfdrive/ui/installer/inter-ascii.ttf rename to openpilot/selfdrive/ui/installer/inter-ascii.ttf diff --git a/selfdrive/ui/body/layouts/__init__.py b/openpilot/selfdrive/ui/layouts/__init__.py similarity index 100% rename from selfdrive/ui/body/layouts/__init__.py rename to openpilot/selfdrive/ui/layouts/__init__.py diff --git a/selfdrive/ui/layouts/home.py b/openpilot/selfdrive/ui/layouts/home.py similarity index 100% rename from selfdrive/ui/layouts/home.py rename to openpilot/selfdrive/ui/layouts/home.py diff --git a/selfdrive/ui/layouts/main.py b/openpilot/selfdrive/ui/layouts/main.py similarity index 95% rename from selfdrive/ui/layouts/main.py rename to openpilot/selfdrive/ui/layouts/main.py index 1e599f4ea7..2b8bac22c2 100644 --- a/selfdrive/ui/layouts/main.py +++ b/openpilot/selfdrive/ui/layouts/main.py @@ -1,6 +1,6 @@ import pyray as rl from enum import IntEnum -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import Widget from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH @@ -34,7 +34,11 @@ class MainLayout(Widget): # Initialize layouts self._home_layout = HomeLayout() self._home_body_layout = BodyLayout() - self._layouts = {MainState.HOME: self._home_layout, MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()} + self._layouts: dict[MainState, Widget] = { + MainState.HOME: self._home_layout, + MainState.SETTINGS: SettingsLayout(), + MainState.ONROAD: AugmentedRoadView(), + } self._sidebar_rect = rl.Rectangle(0, 0, 0, 0) self._content_rect = rl.Rectangle(0, 0, 0, 0) diff --git a/selfdrive/ui/layouts/onboarding.py b/openpilot/selfdrive/ui/layouts/onboarding.py similarity index 96% rename from selfdrive/ui/layouts/onboarding.py rename to openpilot/selfdrive/ui/layouts/onboarding.py index 6e683c0922..df14415c60 100644 --- a/selfdrive/ui/layouts/onboarding.py +++ b/openpilot/selfdrive/ui/layouts/onboarding.py @@ -11,7 +11,7 @@ from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.label import Label from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.version import terms_version, training_version, terms_version_sp +from openpilot.common.version import terms_version, training_version, terms_version_sp from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkOnboarding @@ -53,9 +53,9 @@ class TrainingGuide(Widget): threading.Thread(target=self._preload_thread, daemon=True).start() def _load_image_paths(self): - paths = [fn for fn in os.listdir(os.path.join(BASEDIR, "selfdrive/assets/training")) if re.match(r'^step\d*\.png$', fn)] + paths = [fn for fn in os.listdir(os.path.join(BASEDIR, "openpilot/selfdrive/assets/training")) if re.match(r'^step\d*\.png$', fn)] paths = sorted(paths, key=lambda x: int(re.search(r'\d+', x).group())) - self._image_paths = [os.path.join(BASEDIR, "selfdrive/assets/training", fn) for fn in paths] + self._image_paths = [os.path.join(BASEDIR, "openpilot/selfdrive/assets/training", fn) for fn in paths] def _preload_thread(self): # PNG loading is slow in raylib, so we preload in a thread and upload to GPU in main thread diff --git a/selfdrive/ui/layouts/settings/common.py b/openpilot/selfdrive/ui/layouts/settings/common.py similarity index 100% rename from selfdrive/ui/layouts/settings/common.py rename to openpilot/selfdrive/ui/layouts/settings/common.py diff --git a/selfdrive/ui/layouts/settings/developer.py b/openpilot/selfdrive/ui/layouts/settings/developer.py similarity index 99% rename from selfdrive/ui/layouts/settings/developer.py rename to openpilot/selfdrive/ui/layouts/settings/developer.py index a9fbca768d..ef9c7f9e03 100644 --- a/selfdrive/ui/layouts/settings/developer.py +++ b/openpilot/selfdrive/ui/layouts/settings/developer.py @@ -135,6 +135,7 @@ class DeveloperLayout(Widget): long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad() self._long_maneuver_toggle.action_item.set_enabled(long_man_enabled) + self._lat_maneuver_toggle.action_item.set_enabled(ui_state.is_offroad()) else: self._long_maneuver_toggle.action_item.set_enabled(False) self._lat_maneuver_toggle.action_item.set_enabled(False) diff --git a/selfdrive/ui/layouts/settings/device.py b/openpilot/selfdrive/ui/layouts/settings/device.py similarity index 98% rename from selfdrive/ui/layouts/settings/device.py rename to openpilot/selfdrive/ui/layouts/settings/device.py index 2e0493c8d3..8844623b6e 100644 --- a/selfdrive/ui/layouts/settings/device.py +++ b/openpilot/selfdrive/ui/layouts/settings/device.py @@ -1,7 +1,7 @@ import os import math -from cereal import messaging, log +from openpilot.cereal import messaging, log from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog @@ -105,7 +105,6 @@ class DeviceLayout(Widget): self._params.remove("CalibrationParams") self._params.remove("LiveTorqueParameters") - self._params.remove("LiveParameters") self._params.remove("LiveParametersV2") self._params.remove("LiveDelay") self._params.put_bool("OnroadCycleRequested", True, block=True) @@ -188,7 +187,7 @@ class DeviceLayout(Widget): def _on_regulatory(self): if not self._fcc_dialog: - self._fcc_dialog = HtmlModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html")) + self._fcc_dialog = HtmlModal(os.path.join(BASEDIR, "openpilot/selfdrive/assets/offroad/fcc.html")) gui_app.push_widget(self._fcc_dialog) def _on_review_training_guide(self): diff --git a/selfdrive/ui/layouts/settings/firehose.py b/openpilot/selfdrive/ui/layouts/settings/firehose.py similarity index 100% rename from selfdrive/ui/layouts/settings/firehose.py rename to openpilot/selfdrive/ui/layouts/settings/firehose.py diff --git a/selfdrive/ui/layouts/settings/settings.py b/openpilot/selfdrive/ui/layouts/settings/settings.py similarity index 97% rename from selfdrive/ui/layouts/settings/settings.py rename to openpilot/selfdrive/ui/layouts/settings/settings.py index 68f45df77d..48b75e5dbd 100644 --- a/selfdrive/ui/layouts/settings/settings.py +++ b/openpilot/selfdrive/ui/layouts/settings/settings.py @@ -1,5 +1,5 @@ import pyray as rl -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import IntEnum from collections.abc import Callable from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout @@ -43,7 +43,7 @@ class PanelType(IntEnum): class PanelInfo: name: str instance: Widget - button_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) + button_rect: rl.Rectangle = field(default_factory=lambda: rl.Rectangle(0, 0, 0, 0)) class SettingsLayout(Widget): diff --git a/selfdrive/ui/layouts/settings/software.py b/openpilot/selfdrive/ui/layouts/settings/software.py similarity index 96% rename from selfdrive/ui/layouts/settings/software.py rename to openpilot/selfdrive/ui/layouts/settings/software.py index 4ed9f8fe19..d0016797fc 100644 --- a/selfdrive/ui/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/layouts/settings/software.py @@ -1,4 +1,4 @@ -import os +import subprocess import time import datetime from openpilot.common.time_helpers import system_time_valid @@ -160,12 +160,12 @@ class SoftwareLayout(Widget): # Start checking for updates self._waiting_for_updater = True self._waiting_start_ts = time.monotonic() - os.system("pkill -SIGUSR1 -f system.updated.updated") + subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True) else: # Start downloading self._waiting_for_updater = True self._waiting_start_ts = time.monotonic() - os.system("pkill -SIGHUP -f system.updated.updated") + subprocess.run("pkill -SIGHUP -f openpilot.system.updated.updated", shell=True) def _on_uninstall(self): def handle_uninstall_confirmation(result: DialogResult): @@ -199,7 +199,7 @@ class SoftwareLayout(Widget): selection = self._branch_dialog.selection ui_state.params.put("UpdaterTargetBranch", selection, block=True) self._branch_btn.action_item.set_value(selection) - os.system("pkill -SIGUSR1 -f system.updated.updated") + subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True) self._branch_dialog = None self._branch_dialog = MultiOptionDialog(tr("Select a branch"), branches, current_target, callback=handle_selection) diff --git a/selfdrive/ui/layouts/settings/toggles.py b/openpilot/selfdrive/ui/layouts/settings/toggles.py similarity index 99% rename from selfdrive/ui/layouts/settings/toggles.py rename to openpilot/selfdrive/ui/layouts/settings/toggles.py index 4c83584ad5..ee76b7e4cc 100644 --- a/selfdrive/ui/layouts/settings/toggles.py +++ b/openpilot/selfdrive/ui/layouts/settings/toggles.py @@ -1,4 +1,4 @@ -from cereal import log +from openpilot.cereal import log from openpilot.common.params import Params, UnknownKeyName from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item diff --git a/selfdrive/ui/layouts/sidebar.py b/openpilot/selfdrive/ui/layouts/sidebar.py similarity index 99% rename from selfdrive/ui/layouts/sidebar.py rename to openpilot/selfdrive/ui/layouts/sidebar.py index 3405667245..f950edaa46 100644 --- a/selfdrive/ui/layouts/sidebar.py +++ b/openpilot/selfdrive/ui/layouts/sidebar.py @@ -2,7 +2,7 @@ import pyray as rl import time from dataclasses import dataclass from collections.abc import Callable -from cereal import log +from openpilot.cereal import log from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, FONT_SCALE from openpilot.system.ui.lib.multilang import tr, tr_noop @@ -68,7 +68,7 @@ class Sidebar(Widget, SidebarSP): def __init__(self): Widget.__init__(self) SidebarSP.__init__(self) - self._net_type = NETWORK_TYPES.get(NetworkType.none) + self._net_type = NETWORK_TYPES[NetworkType.none] self._net_strength = 0 self._temp_status = MetricData(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD) diff --git a/selfdrive/ui/lib/api_helpers.py b/openpilot/selfdrive/ui/lib/api_helpers.py similarity index 100% rename from selfdrive/ui/lib/api_helpers.py rename to openpilot/selfdrive/ui/lib/api_helpers.py diff --git a/selfdrive/ui/lib/prime_state.py b/openpilot/selfdrive/ui/lib/prime_state.py similarity index 100% rename from selfdrive/ui/lib/prime_state.py rename to openpilot/selfdrive/ui/lib/prime_state.py diff --git a/selfdrive/ui/layouts/__init__.py b/openpilot/selfdrive/ui/mici/layouts/__init__.py similarity index 100% rename from selfdrive/ui/layouts/__init__.py rename to openpilot/selfdrive/ui/mici/layouts/__init__.py diff --git a/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py similarity index 98% rename from selfdrive/ui/mici/layouts/home.py rename to openpilot/selfdrive/ui/mici/layouts/home.py index d41165a79d..4c4dfac520 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -1,7 +1,7 @@ import datetime import time -from cereal import log +from openpilot.cereal import log import pyray as rl from collections.abc import Callable from openpilot.system.ui.widgets import Widget @@ -10,7 +10,7 @@ from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.version import RELEASE_BRANCHES +from openpilot.common.version import RELEASE_BRANCHES HEAD_BUTTON_FONT_SIZE = 40 HOME_PADDING = 8 @@ -251,7 +251,7 @@ class MiciHomeLayout(Widget): self._egpu_icon.set_visible(ui_state.usbgpu and ui_state.usbgpu_compiled) self._egpu_icon_gray.set_visible(ui_state.usbgpu and not ui_state.usbgpu_compiled) self._mic_icon.set_visible(ui_state.recording_audio) - self._body_icon.set_visible(ui_state.is_body) + self._body_icon.set_visible(bool(ui_state.is_body)) footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48) self._status_bar_layout.render(footer_rect) diff --git a/selfdrive/ui/mici/layouts/main.py b/openpilot/selfdrive/ui/mici/layouts/main.py similarity index 98% rename from selfdrive/ui/mici/layouts/main.py rename to openpilot/selfdrive/ui/mici/layouts/main.py index e25b2840f7..6356a2bc9d 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/openpilot/selfdrive/ui/mici/layouts/main.py @@ -1,5 +1,5 @@ import pyray as rl -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts @@ -148,4 +148,4 @@ class MiciMainLayout(Scroller): def _on_body_changed(self): self._car_onroad_layout.set_visible(not ui_state.is_body) - self._body_onroad_layout.set_visible(ui_state.is_body) + self._body_onroad_layout.set_visible(bool(ui_state.is_body)) diff --git a/selfdrive/ui/mici/layouts/offroad_alerts.py b/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py similarity index 97% rename from selfdrive/ui/mici/layouts/offroad_alerts.py rename to openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py index e0c574dc07..879ac76df5 100644 --- a/selfdrive/ui/mici/layouts/offroad_alerts.py +++ b/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py @@ -7,7 +7,7 @@ from enum import IntEnum from openpilot.common.params import Params from openpilot.common.realtime import drop_realtime from openpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.scroller import Scroller @@ -250,12 +250,12 @@ class MiciOffroadAlerts(Scroller): {alert_data.key: self.params.get(alert_data.key) for alert_data in self.sorted_alerts}) time.sleep(REFRESH_INTERVAL) - def _refresh(self) -> int: + def _refresh(self, pending_params: dict) -> int: """Refresh alerts from params and return active count.""" active_count = 0 # Handle UpdateAvailable alert specially - update_available = self._pending_params["UpdateAvailable"] + update_available = pending_params["UpdateAvailable"] update_alert_data = next((alert_data for alert_data in self.sorted_alerts if alert_data.key == "UpdateAvailable"), None) if update_alert_data: @@ -263,7 +263,7 @@ class MiciOffroadAlerts(Scroller): version_string = "" # Get new version description and parse version and date - new_desc = self._pending_params["UpdaterNewDescription"] or "" + new_desc = pending_params["UpdaterNewDescription"] or "" if new_desc: # format: "version / branch / commit / date" parts = new_desc.split(" / ") @@ -284,7 +284,7 @@ class MiciOffroadAlerts(Scroller): continue # Skip, already handled above text = "" - alert_json = self._pending_params[alert_data.key] + alert_json = pending_params[alert_data.key] if alert_json: text = alert_json.get("text", "").replace("%1", alert_json.get("extra", "")) @@ -311,8 +311,9 @@ class MiciOffroadAlerts(Scroller): def _update_state(self): """Periodically refresh alerts.""" # Refresh alerts when thread updates params - if self._pending_params is not None: - self._refresh() + pending_params = self._pending_params + if pending_params is not None: + self._refresh(pending_params) self._pending_params = None def _render(self, rect: rl.Rectangle): diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/openpilot/selfdrive/ui/mici/layouts/onboarding.py similarity index 99% rename from selfdrive/ui/mici/layouts/onboarding.py rename to openpilot/selfdrive/ui/mici/layouts/onboarding.py index f070ec5d5c..3c5ad27a17 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/openpilot/selfdrive/ui/mici/layouts/onboarding.py @@ -12,8 +12,8 @@ from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.mici_setup import GreyBigButton, BigPillButton from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.lib.multilang import tr -from openpilot.system.version import terms_version, training_version, terms_version_sp -from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined +from openpilot.common.version import terms_version, training_version, terms_version_sp +from openpilot.common.version import sunnylink_consent_version, sunnylink_consent_declined from openpilot.selfdrive.ui.ui_state import ui_state, device from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationCircleButton from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/openpilot/selfdrive/ui/mici/layouts/settings/developer.py similarity index 99% rename from selfdrive/ui/mici/layouts/settings/developer.py rename to openpilot/selfdrive/ui/mici/layouts/settings/developer.py index 2f0250ff5f..2d7dc0899d 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/developer.py @@ -152,6 +152,7 @@ class DeveloperLayoutMici(NavScroller): long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad() self._long_maneuver_toggle.set_enabled(long_man_enabled) + self._lat_maneuver_toggle.set_enabled(ui_state.is_offroad()) else: self._long_maneuver_toggle.set_enabled(False) self._lat_maneuver_toggle.set_enabled(False) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/openpilot/selfdrive/ui/mici/layouts/settings/device.py similarity index 64% rename from selfdrive/ui/mici/layouts/settings/device.py rename to openpilot/selfdrive/ui/mici/layouts/settings/device.py index 4a8fc50d53..7f4a9ab0b8 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/device.py @@ -1,7 +1,5 @@ import os -import threading import pyray as rl -from enum import IntEnum from collections.abc import Callable from openpilot.common.basedir import BASEDIR @@ -18,7 +16,7 @@ from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer +from openpilot.system.ui.widgets.html_render import HtmlRenderer from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID @@ -122,12 +120,6 @@ class DeviceInfoLayoutMici(Widget): self._serial_number_text_label.render() -class UpdaterState(IntEnum): - IDLE = 0 - WAITING_FOR_UPDATER = 1 - UPDATER_RESPONDING = 2 - - class PairBigButton(BigButton): def __init__(self): super().__init__("pair", "connect.comma.ai", gui_app.texture("icons_mici/settings/comma_icon.png", 33, 60)) @@ -164,133 +156,11 @@ class PairBigButton(BigButton): gui_app.push_widget(dlg) -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, 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) - - self._waiting_for_updater_t: float | None = None - self._hide_value_t: float | None = None - self._state: UpdaterState = UpdaterState.IDLE - - ui_state.add_offroad_transition_callback(self.offroad_transition) - - def offroad_transition(self): - if ui_state.is_offroad(): - self.set_enabled(True) - - def _handle_mouse_release(self, mouse_pos: MousePos): - super()._handle_mouse_release(mouse_pos) - - if not system_time_valid(): - dlg = BigDialog("", tr("Please connect to Wi-Fi to update.")) - gui_app.push_widget(dlg) - return - - self.set_enabled(False) - self._state = UpdaterState.WAITING_FOR_UPDATER - self.set_icon(self._txt_update_icon) - - def run(): - if self.get_value() == "download update": - os.system("pkill -SIGHUP -f system.updated.updated") - elif self.get_value() == "update now": - ui_state.params.put_bool("DoReboot", True, block=True) - else: - os.system("pkill -SIGUSR1 -f system.updated.updated") - - threading.Thread(target=run, daemon=True).start() - - def set_value(self, value: str): - super().set_value(value) - if value: - self.set_text("") - else: - self.set_text("update sunnypilot") - - def _update_state(self): - super()._update_state() - - if ui_state.started: - self.set_enabled(False) - return - - updater_state = ui_state.params.get("UpdaterState") or "" - failed_count = ui_state.params.get("UpdateFailedCount") - failed = False if failed_count is None else int(failed_count) > 0 - - if ui_state.params.get_bool("UpdateAvailable"): - self.set_rotate_icon(False) - self.set_enabled(True) - if self.get_value() != "update now": - self.set_value("update now") - self.set_icon(self._txt_reboot_icon) - - elif self._state == UpdaterState.WAITING_FOR_UPDATER: - self.set_rotate_icon(True) - if updater_state != "idle": - self._state = UpdaterState.UPDATER_RESPONDING - - # Recover from updater not responding (time invalid shortly after boot) - if self._waiting_for_updater_t is None: - self._waiting_for_updater_t = rl.get_time() - - 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\nto respond") - self._state = UpdaterState.IDLE - self._hide_value_t = rl.get_time() - - elif self._state == UpdaterState.UPDATER_RESPONDING: - if updater_state == "idle": - self.set_rotate_icon(False) - self._state = UpdaterState.IDLE - self._hide_value_t = rl.get_time() - else: - if self.get_value() != updater_state: - self.set_value(updater_state) - - elif self._state == UpdaterState.IDLE: - self.set_rotate_icon(False) - if failed: - self.set_enabled(True) # allow retry when failure came from updater param - if self.get_value() != "failed to update": - self.set_value("failed to update") - - elif ui_state.params.get_bool("UpdaterFetchAvailable"): - self.set_enabled(True) - if self.get_value() != "download update": - self.set_value("download update") - - elif self._hide_value_t is not None: - self.set_enabled(True) - if self.get_value() == "checking...": - self.set_value("up to date") - self.set_icon(self._txt_up_to_date_icon) - - # Hide previous text after short amount of time (up to date or failed) - if rl.get_time() - self._hide_value_t > 3.0: - self._hide_value_t = None - self.set_value("") - self.set_icon(self._txt_update_icon) - else: - if self.get_value() != "": - self.set_value("") - - if self._state != UpdaterState.WAITING_FOR_UPDATER: - self._waiting_for_updater_t = None - - class DeviceLayoutMici(NavScroller): def __init__(self): super().__init__() - self._fcc_dialog: HtmlModal | None = None + self._fcc_dialog: MiciFccModal | None = None def power_off_callback(): ui_state.params.put_bool("DoShutdown", True, block=True) @@ -302,21 +172,13 @@ class DeviceLayoutMici(NavScroller): params = ui_state.params params.remove("CalibrationParams") params.remove("LiveTorqueParameters") - params.remove("LiveParameters") params.remove("LiveParametersV2") params.remove("LiveDelay") params.put_bool("OnroadCycleRequested", True, block=True) - def uninstall_openpilot_callback(): - ui_state.params.put_bool("DoUninstall", True, block=True) - reset_calibration_btn = EngagedConfirmationButton("reset calibration", "reset", gui_app.texture("icons_mici/settings/device/lkas.png", 122, 64), reset_calibration_callback) - uninstall_openpilot_btn = EngagedConfirmationButton("uninstall sunnypilot", "uninstall", - gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64), - uninstall_openpilot_callback, exit_on_confirm=False) - reboot_btn = EngagedConfirmationCircleButton("reboot", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70), reboot_callback, exit_on_confirm=False) @@ -340,19 +202,17 @@ class DeviceLayoutMici(NavScroller): self._scroller.add_widgets([ DeviceInfoLayoutMici(), - UpdateOpenpilotBigButton(), PairBigButton(), review_training_guide_btn, driver_cam_btn, terms_btn, regulatory_btn, reset_calibration_btn, - uninstall_openpilot_btn, reboot_btn, self._power_off_btn, ]) def _on_regulatory(self): if not self._fcc_dialog: - self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html")) + self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "openpilot/selfdrive/assets/offroad/mici_fcc.html")) gui_app.push_widget(self._fcc_dialog) diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/openpilot/selfdrive/ui/mici/layouts/settings/firehose.py similarity index 100% rename from selfdrive/ui/mici/layouts/settings/firehose.py rename to openpilot/selfdrive/ui/mici/layouts/settings/firehose.py diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/openpilot/selfdrive/ui/mici/layouts/settings/network/__init__.py similarity index 100% rename from selfdrive/ui/mici/layouts/settings/network/__init__.py rename to openpilot/selfdrive/ui/mici/layouts/settings/network/__init__.py diff --git a/selfdrive/ui/mici/layouts/settings/network/network_layout.py b/openpilot/selfdrive/ui/mici/layouts/settings/network/network_layout.py similarity index 100% rename from selfdrive/ui/mici/layouts/settings/network/network_layout.py rename to openpilot/selfdrive/ui/mici/layouts/settings/network/network_layout.py diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/openpilot/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py similarity index 100% rename from selfdrive/ui/mici/layouts/settings/network/wifi_ui.py rename to openpilot/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/openpilot/selfdrive/ui/mici/layouts/settings/settings.py similarity index 87% rename from selfdrive/ui/mici/layouts/settings/settings.py rename to openpilot/selfdrive/ui/mici/layouts/settings/settings.py index 4ccc5ba139..56a953a65d 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/settings.py @@ -5,6 +5,7 @@ from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMi from openpilot.selfdrive.ui.mici.layouts.settings.network.network_layout import NetworkLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici +from openpilot.selfdrive.ui.mici.layouts.settings.software import SoftwareLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout from openpilot.system.ui.lib.application import gui_app, FontWeight @@ -31,6 +32,10 @@ class SettingsLayout(NavScroller): device_btn = SettingsBigButton("device", "", gui_app.texture("icons_mici/settings/device_icon.png", 72, 58)) device_btn.set_click_callback(lambda: gui_app.push_widget(device_panel)) + software_panel = SoftwareLayoutMici() + software_btn = SettingsBigButton("software", "", gui_app.texture("icons_mici/settings/software.png", 64, 75)) + software_btn.set_click_callback(lambda: gui_app.push_widget(software_panel)) + developer_panel = DeveloperLayoutMici() developer_btn = SettingsBigButton("developer", "", gui_app.texture("icons_mici/settings/developer_icon.png", 64, 60)) developer_btn.set_click_callback(lambda: gui_app.push_widget(developer_panel)) @@ -43,6 +48,7 @@ class SettingsLayout(NavScroller): toggles_btn, network_btn, device_btn, + software_btn, PairBigButton(), #BigDialogButton("manual", "", "icons_mici/settings/manual_icon.png", "Check out the mici user\nmanual at comma.ai/setup"), firehose_btn, diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py new file mode 100644 index 0000000000..d71982ca26 --- /dev/null +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -0,0 +1,274 @@ +import subprocess +import threading +import pyray as rl +from enum import IntEnum +from collections.abc import Callable + +from openpilot.common.time_helpers import system_time_valid +from openpilot.selfdrive.ui.mici.layouts.settings.device import EngagedConfirmationButton +from openpilot.selfdrive.ui.mici.widgets.button import BigButton +from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.label import UnifiedLabel +from openpilot.system.ui.widgets.scroller import NavScroller + +UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond + + +def _split_description(desc: str) -> tuple[str, str, str, str] | None: + # UpdaterCurrentDescription/UpdaterNewDescription format: "version / branch / commit / date" + parts = [p.strip() for p in desc.split(" / ")] + if len(parts) != 4: + return None + version, branch, commit, date = parts + return version, branch, commit, date + + +class UpdaterState(IntEnum): + IDLE = 0 + WAITING_FOR_UPDATER = 1 + UPDATER_RESPONDING = 2 + + +class SoftwareInfoLayoutMici(Widget): + def __init__(self): + super().__init__() + + self.set_rect(rl.Rectangle(0, 0, 360, 180)) + + subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) + max_width = int(self._rect.width - 20) + self._version_label = UnifiedLabel("version", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) + self._version_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color, + font_weight=FontWeight.ROMAN, wrap_text=False) + + self._branch_label = UnifiedLabel("branch", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) + self._branch_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color, + font_weight=FontWeight.ROMAN, wrap_text=False) + + def _update_state(self): + desc = _split_description(ui_state.params.get("UpdaterCurrentDescription") or "") + if desc is not None: + version, branch, commit, date = desc + self._version_text_label.set_text(f"{version} ({date})") + self._branch_text_label.set_text(f"{branch} ({commit})") + else: + self._version_text_label.set_text(ui_state.params.get("Version") or "N/A") + self._branch_text_label.set_text(ui_state.params.get("GitBranch") or "N/A") + + def _render(self, _): + self._version_label.set_position(self._rect.x + 20, self._rect.y - 10) + self._version_label.render() + + self._version_text_label.set_position(self._rect.x + 20, self._rect.y + 68 - 25) + self._version_text_label.render() + + self._branch_label.set_position(self._rect.x + 20, self._rect.y + 114 - 30) + self._branch_label.render() + + self._branch_text_label.set_position(self._rect.x + 20, self._rect.y + 161 - 25) + self._branch_text_label.render() + + +class CheckUpdateButton(BigButton): + def __init__(self): + self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75) + self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) + super().__init__("check for update", "", self._txt_update_icon) + + self._waiting_for_updater_t: float | None = None + self._hide_value_t: float | None = None + self._state: UpdaterState = UpdaterState.IDLE + + ui_state.add_offroad_transition_callback(self.offroad_transition) + + def offroad_transition(self): + if ui_state.is_offroad(): + self.set_enabled(True) + + def _handle_mouse_release(self, mouse_pos: MousePos): + super()._handle_mouse_release(mouse_pos) + + if not system_time_valid(): + dlg = BigDialog("", tr("Please connect to Wi-Fi to update.")) + gui_app.push_widget(dlg) + return + + self.set_enabled(False) + self._state = UpdaterState.WAITING_FOR_UPDATER + self.set_icon(self._txt_update_icon) + + def run(): + if self.get_value() == "download update": + subprocess.run("pkill -SIGHUP -f openpilot.system.updated.updated", shell=True) + else: + subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True) + + threading.Thread(target=run, daemon=True).start() + + def set_value(self, value: str): + super().set_value(value) + if value: + self.set_text("") + else: + self.set_text("check for update") + + def _update_state(self): + super()._update_state() + + if ui_state.started: + self.set_enabled(False) + return + + updater_state = ui_state.params.get("UpdaterState") or "" + failed_count = ui_state.params.get("UpdateFailedCount") or 0 + failed = int(failed_count) > 0 + + if self._state == UpdaterState.WAITING_FOR_UPDATER: + self.set_rotate_icon(True) + if updater_state != "idle": + self._state = UpdaterState.UPDATER_RESPONDING + + # Recover from updater not responding (time invalid shortly after boot) + if self._waiting_for_updater_t is None: + self._waiting_for_updater_t = rl.get_time() + + 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\nto respond") + self._state = UpdaterState.IDLE + self._hide_value_t = rl.get_time() + + elif self._state == UpdaterState.UPDATER_RESPONDING: + if updater_state == "idle": + self.set_rotate_icon(False) + self._state = UpdaterState.IDLE + self._hide_value_t = rl.get_time() + else: + if self.get_value() != updater_state: + self.set_value(updater_state) + + elif self._state == UpdaterState.IDLE: + self.set_rotate_icon(False) + if failed: + self.set_enabled(True) # allow retry when failure came from updater param + if self.get_value() != "failed to update": + self.set_value("failed to update") + + elif ui_state.params.get_bool("UpdaterFetchAvailable"): + self.set_enabled(True) + if self.get_value() != "download update": + self.set_value("download update") + + elif self._hide_value_t is not None: + self.set_enabled(True) + if self.get_value() == "checking...": + self.set_value("up to date") + self.set_icon(self._txt_up_to_date_icon) + + # Hide previous text after short amount of time (up to date or failed) + if rl.get_time() - self._hide_value_t > 3.0: + self._hide_value_t = None + self.set_value("") + self.set_icon(self._txt_update_icon) + else: + if self.get_value() != "": + self.set_value("") + + if self._state != UpdaterState.WAITING_FOR_UPDATER: + self._waiting_for_updater_t = None + + +class InstallUpdateButton(BigButton): + def __init__(self): + super().__init__("install update", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70)) + self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable")) + + def _update_state(self): + super()._update_state() + + desc = _split_description(ui_state.params.get("UpdaterNewDescription") or "") + value = f"{desc[0]} ({desc[1]})" if desc is not None else "" + if self.get_value() != value: + self.set_value(value) + + def _handle_mouse_release(self, mouse_pos: MousePos): + super()._handle_mouse_release(mouse_pos) + + self.set_enabled(False) + + def run(): + ui_state.params.put_bool("DoReboot", True, block=True) + + threading.Thread(target=run, daemon=True).start() + + +class BranchSelectPage(NavScroller): + def __init__(self, on_select: Callable[[str], None]): + super().__init__() + + params = ui_state.params + current_git_branch = params.get("GitBranch") or "" + branches_str = params.get("UpdaterAvailableBranches") or "" + branches = [b for b in branches_str.split(",") if b] + + for b in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]: + if b in branches: + branches.remove(b) + branches.insert(0, b) + + current_target = params.get("UpdaterTargetBranch") or "" + check_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) + + buttons = [] + for branch in branches: + btn = BigButton(branch, "", check_icon if branch == current_target else None, scroll=True) + btn.set_click_callback(lambda b=branch: self.dismiss(lambda: on_select(b))) + buttons.append(btn) + self._scroller.add_widgets(buttons) + + +class TargetBranchButton(BigButton): + def __init__(self): + super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "") + self.set_click_callback(self._on_click) + self.set_visible(not ui_state.params.get_bool("IsTestedBranch")) + self.set_enabled(lambda: ui_state.is_offroad()) + + def _update_state(self): + super()._update_state() + + target = ui_state.params.get("UpdaterTargetBranch") or "" + if self.get_value() != target: + self.set_value(target) + + def _on_click(self): + gui_app.push_widget(BranchSelectPage(self._on_select)) + + def _on_select(self, branch: str): + ui_state.params.put("UpdaterTargetBranch", branch, block=True) + self.set_value(branch) + subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True) + + +class SoftwareLayoutMici(NavScroller): + def __init__(self): + super().__init__() + + def uninstall_openpilot_callback(): + ui_state.params.put_bool("DoUninstall", True, block=True) + + uninstall_openpilot_btn = EngagedConfirmationButton("uninstall sunnypilot", "uninstall", + gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64), + uninstall_openpilot_callback, exit_on_confirm=False) + + self._scroller.add_widgets([ + SoftwareInfoLayoutMici(), + CheckUpdateButton(), + InstallUpdateButton(), + TargetBranchButton(), + uninstall_openpilot_btn, + ]) diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py similarity index 99% rename from selfdrive/ui/mici/layouts/settings/toggles.py rename to openpilot/selfdrive/ui/mici/layouts/settings/toggles.py index 8635336f97..52bbb65e6a 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py @@ -1,4 +1,4 @@ -from cereal import log +from openpilot.cereal import log from openpilot.system.ui.widgets.scroller import NavScroller from openpilot.selfdrive.ui.mici.widgets.button import BigParamControl, BigMultiParamToggle diff --git a/selfdrive/ui/mici/onroad/__init__.py b/openpilot/selfdrive/ui/mici/onroad/__init__.py similarity index 100% rename from selfdrive/ui/mici/onroad/__init__.py rename to openpilot/selfdrive/ui/mici/onroad/__init__.py diff --git a/selfdrive/ui/mici/onroad/alert_renderer.py b/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py similarity index 99% rename from selfdrive/ui/mici/onroad/alert_renderer.py rename to openpilot/selfdrive/ui/mici/onroad/alert_renderer.py index 5b550030de..016e09a0f1 100644 --- a/selfdrive/ui/mici/onroad/alert_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py @@ -5,10 +5,11 @@ import pyray as rl import random import string from dataclasses import dataclass -from cereal import messaging, log, car +from openpilot.cereal import messaging, log +from opendbc.car.structs import car from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py similarity index 98% rename from selfdrive/ui/mici/onroad/augmented_road_view.py rename to openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py index 8dafc4df57..b5b9a54be8 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -1,6 +1,7 @@ import numpy as np import pyray as rl -from cereal import car, log +from openpilot.cereal import log +from opendbc.car.structs import car from msgq.visionipc import VisionStreamType from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH @@ -317,10 +318,13 @@ class AugmentedRoadView(CameraView): w, h = self._content_rect.width, self._content_rect.height cx, cy = intrinsic[0, 2], intrinsic[1, 2] + # Ensure zoom views the whole area + zoom = max(zoom, w / (2 * cx), h / (2 * cy)) + # Calculate max allowed offsets with margins margin = 5 - max_x_offset = cx * zoom - w / 2 - margin - max_y_offset = cy * zoom - h / 2 - margin + max_x_offset = max(0.0, cx * zoom - w / 2 - margin) + max_y_offset = max(0.0, cy * zoom - h / 2 - margin) # Calculate and clamp offsets to prevent out-of-bounds issues try: diff --git a/selfdrive/ui/mici/onroad/cameraview.py b/openpilot/selfdrive/ui/mici/onroad/cameraview.py similarity index 99% rename from selfdrive/ui/mici/onroad/cameraview.py rename to openpilot/selfdrive/ui/mici/onroad/cameraview.py index 86dd95f466..82e4865c76 100644 --- a/selfdrive/ui/mici/onroad/cameraview.py +++ b/openpilot/selfdrive/ui/mici/onroad/cameraview.py @@ -4,7 +4,7 @@ import pyray as rl from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage from openpilot.system.ui.widgets import Widget @@ -219,7 +219,7 @@ class CameraView(Widget): [0.0, 0.0, 1.0] ]) - def _render(self, rect: rl.Rectangle): + def _render(self, rect: rl.Rectangle, /): if self._switching: self._handle_switch() diff --git a/selfdrive/ui/mici/onroad/confidence_ball.py b/openpilot/selfdrive/ui/mici/onroad/confidence_ball.py similarity index 100% rename from selfdrive/ui/mici/onroad/confidence_ball.py rename to openpilot/selfdrive/ui/mici/onroad/confidence_ball.py diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/openpilot/selfdrive/ui/mici/onroad/driver_camera_dialog.py similarity index 98% rename from selfdrive/ui/mici/onroad/driver_camera_dialog.py rename to openpilot/selfdrive/ui/mici/onroad/driver_camera_dialog.py index df5afe2e7c..e81877b402 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/openpilot/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -1,5 +1,5 @@ import pyray as rl -from cereal import car, log, messaging +from openpilot.cereal import log, messaging from msgq.visionipc import VisionStreamType from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer @@ -104,8 +104,9 @@ class BaseDriverCameraDialog(Widget): if self._pm is None: return - AudibleAlert = car.CarControl.HUDControl.AudibleAlert + AudibleAlert = log.SelfdriveState.AudibleAlert ALERT_SOUNDS = { + 'one': AudibleAlert.preAlert, 'two': AudibleAlert.promptDistracted, 'three': AudibleAlert.warningImmediate, } diff --git a/selfdrive/ui/mici/onroad/driver_state.py b/openpilot/selfdrive/ui/mici/onroad/driver_state.py similarity index 99% rename from selfdrive/ui/mici/onroad/driver_state.py rename to openpilot/selfdrive/ui/mici/onroad/driver_state.py index 1855d77b77..85b90704ac 100644 --- a/selfdrive/ui/mici/onroad/driver_state.py +++ b/openpilot/selfdrive/ui/mici/onroad/driver_state.py @@ -1,7 +1,7 @@ import pyray as rl import numpy as np import math -from cereal import log +from openpilot.cereal import log from openpilot.common.filter_simple import FirstOrderFilter from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import Widget diff --git a/selfdrive/ui/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py similarity index 99% rename from selfdrive/ui/mici/onroad/hud_renderer.py rename to openpilot/selfdrive/ui/mici/onroad/hud_renderer.py index c74d3101b0..67f9833021 100644 --- a/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py @@ -8,7 +8,7 @@ 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 from openpilot.common.filter_simple import FirstOrderFilter -from cereal import log +from openpilot.cereal import log EventName = log.OnroadEvent.EventName diff --git a/selfdrive/ui/mici/onroad/model_renderer.py b/openpilot/selfdrive/ui/mici/onroad/model_renderer.py similarity index 98% rename from selfdrive/ui/mici/onroad/model_renderer.py rename to openpilot/selfdrive/ui/mici/onroad/model_renderer.py index e88d320a3f..3f514b28d2 100644 --- a/selfdrive/ui/mici/onroad/model_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/model_renderer.py @@ -1,7 +1,8 @@ import colorsys import numpy as np import pyray as rl -from cereal import messaging, car +from openpilot.cereal import messaging +from opendbc.car.structs import car from dataclasses import dataclass, field from openpilot.common.params import Params from openpilot.common.filter_simple import FirstOrderFilter @@ -46,8 +47,8 @@ class ModelPoints: @dataclass class LeadVehicle: - glow: list[float] = field(default_factory=list) - chevron: list[float] = field(default_factory=list) + glow: list[tuple[float, float]] = field(default_factory=list) + chevron: list[tuple[float, float]] = field(default_factory=list) fill_alpha: int = 0 @@ -176,7 +177,7 @@ class ModelRenderer(Widget, ModelRendererSP): leads = [radar_state.leadOne, radar_state.leadTwo] for i, lead_data in enumerate(leads): - if lead_data and lead_data.status: + if lead_data and lead_data.present: d_rel, y_rel, v_rel = lead_data.dRel, lead_data.yRel, lead_data.vRel idx = self._get_path_length_idx(path_x_array, d_rel) @@ -205,7 +206,7 @@ class ModelRenderer(Widget, ModelRendererSP): road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, line_width_factor, 0.0, max_idx) # Update path using raw points - if lead and lead.status: + if lead and lead.present: lead_d = lead.dRel * 2.0 max_distance = np.clip(lead_d - min(lead_d * 0.35, 10.0), 0.0, max_distance) diff --git a/selfdrive/ui/mici/onroad/torque_bar.py b/openpilot/selfdrive/ui/mici/onroad/torque_bar.py similarity index 99% rename from selfdrive/ui/mici/onroad/torque_bar.py rename to openpilot/selfdrive/ui/mici/onroad/torque_bar.py index 861bfe493e..85edd89e98 100644 --- a/selfdrive/ui/mici/onroad/torque_bar.py +++ b/openpilot/selfdrive/ui/mici/onroad/torque_bar.py @@ -165,7 +165,7 @@ class TorqueBar(Widget): return # torque line - if ui_state.sm['controlsState'].lateralControlState.which() == 'angleState': + if ui_state.sm['controlsState'].lateralControlState.which() in ('angleState', 'curvatureState'): controls_state = ui_state.sm['controlsState'] car_state = ui_state.sm['carState'] live_parameters = ui_state.sm['liveParameters'] diff --git a/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py b/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py new file mode 100755 index 0000000000..68c61f4ff3 --- /dev/null +++ b/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py @@ -0,0 +1,121 @@ +import gc +import weakref +import pytest + +# FIXME: known small leaks not worth worrying about at the moment +KNOWN_LEAKS = { + "openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog.DriverCameraView", + "openpilot.selfdrive.ui.mici.layouts.onboarding.TermsPage", + "openpilot.selfdrive.ui.mici.layouts.onboarding.TrainingGuide", + "openpilot.selfdrive.ui.mici.layouts.onboarding.DeclinePage", + "openpilot.selfdrive.ui.mici.layouts.onboarding.OnboardingWindow", + "openpilot.selfdrive.ui.onroad.driver_state.DriverStateRenderer", + "openpilot.selfdrive.ui.onroad.driver_camera_dialog.DriverCameraDialog", + "openpilot.selfdrive.ui.layouts.onboarding.TermsPage", + "openpilot.selfdrive.ui.layouts.onboarding.DeclinePage", + "openpilot.selfdrive.ui.layouts.onboarding.OnboardingWindow", + "openpilot.system.ui.widgets.confirm_dialog.ConfirmDialog", + "openpilot.system.ui.widgets.label.Label", + "openpilot.system.ui.widgets.button.Button", + "openpilot.system.ui.widgets.html_render.HtmlRenderer", + "openpilot.system.ui.widgets.nav_widget.NavBar", + "openpilot.selfdrive.ui.mici.layouts.settings.device.MiciFccModal", + "openpilot.system.ui.widgets.inputbox.InputBox", + "openpilot.system.ui.widgets.scroller_tici.Scroller", + "openpilot.system.ui.widgets.label.UnifiedLabel", + "openpilot.system.ui.widgets.mici_keyboard.MiciKeyboard", + "openpilot.selfdrive.ui.mici.widgets.dialog.BigConfirmationDialog", + "openpilot.system.ui.widgets.keyboard.Keyboard", + "openpilot.system.ui.widgets.slider.BigSlider", + "openpilot.selfdrive.ui.mici.widgets.dialog.BigInputDialog", + "openpilot.system.ui.widgets.option_dialog.MultiOptionDialog", +} + + +def get_child_widgets(widget) -> list: + from openpilot.system.ui.widgets import Widget + children = [] + for val in widget.__dict__.values(): + items = val if isinstance(val, (list, tuple)) else (val,) + children.extend(w for w in items if isinstance(w, Widget)) + return children + + +class TestWidgetLeaks: + @pytest.mark.skip(reason="segfaults") + def test_dialogs_do_not_leak(self): + import pyray as rl + rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN) + from openpilot.system.ui.lib.application import gui_app + + # mici dialogs + from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow + from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog + from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog + from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog + from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal + + # tici dialogs + from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog + from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow + from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog + from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog + from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog + from openpilot.system.ui.widgets.html_render import HtmlModal + from openpilot.system.ui.widgets.keyboard import Keyboard + + gui_app.init_window("ref-test") + + leaked_widgets = set() + + for ctor in ( + # mici + MiciDriverCameraDialog, MiciPairingDialog, + lambda: MiciTrainingGuide(lambda: None), + lambda: MiciOnboardingWindow(lambda: None), + lambda: BigDialog("test", "test"), + lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None), + lambda: BigInputDialog("test"), + lambda: MiciFccModal(text="test"), + # tici + TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard, + lambda: ConfirmDialog("test", "ok"), + lambda: MultiOptionDialog("test", ["a", "b"]), + lambda: HtmlModal(text="test"), + ): + widget = ctor() + all_refs = [weakref.ref(w) for w in get_child_widgets(widget) + [widget]] + + del widget + + for ref in all_refs: + if ref() is not None: + obj = ref() + name = f"{type(obj).__module__}.{type(obj).__qualname__}" + leaked_widgets.add(name) + + print(f"\n=== Widget {name} alive after del") + print(" Referrers:") + for r in gc.get_referrers(obj): + if r is obj: + continue + + if hasattr(r, '__self__') and r.__self__ is not obj: + print(f" bound method: {type(r.__self__).__qualname__}.{r.__name__}") + elif hasattr(r, '__func__'): + print(f" method: {r.__name__}") + else: + print(f" {type(r).__module__}.{type(r).__qualname__}") + del obj + + gui_app.close() + + unexpected = leaked_widgets - KNOWN_LEAKS + assert not unexpected, f"New leaked widgets: {unexpected}" + + fixed = KNOWN_LEAKS - leaked_widgets + assert not fixed, f"These leaks are fixed, remove from KNOWN_LEAKS: {fixed}" + + +if __name__ == "__main__": + TestWidgetLeaks().test_dialogs_do_not_leak() diff --git a/selfdrive/ui/mici/widgets/button.py b/openpilot/selfdrive/ui/mici/widgets/button.py similarity index 99% rename from selfdrive/ui/mici/widgets/button.py rename to openpilot/selfdrive/ui/mici/widgets/button.py index 1dceb79691..3dd7ad9a8a 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/openpilot/selfdrive/ui/mici/widgets/button.py @@ -375,6 +375,7 @@ class GreyBigButton(BigButton): class BigMultiParamToggle(BigMultiToggle): def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable | None = None, select_callback: Callable | None = None): + assert Params is not None super().__init__(text, options, toggle_callback, select_callback) self._param = param @@ -392,6 +393,7 @@ class BigMultiParamToggle(BigMultiToggle): class BigParamControl(BigToggle): def __init__(self, text: str, param: str, toggle_callback: Callable | None = None): + assert Params is not None super().__init__(text, "", toggle_callback=toggle_callback) self.param = param self.params = Params() @@ -409,6 +411,7 @@ class BigParamControl(BigToggle): class BigCircleParamControl(BigCircleToggle): def __init__(self, icon: rl.Texture, param: str, toggle_callback: Callable | None = None, icon_offset: tuple[int, int] = (0, 0)): + assert Params is not None super().__init__(icon, toggle_callback, icon_offset=icon_offset) self._param = param self.params = Params() diff --git a/selfdrive/ui/mici/widgets/dialog.py b/openpilot/selfdrive/ui/mici/widgets/dialog.py similarity index 100% rename from selfdrive/ui/mici/widgets/dialog.py rename to openpilot/selfdrive/ui/mici/widgets/dialog.py diff --git a/selfdrive/ui/mici/widgets/pairing_dialog.py b/openpilot/selfdrive/ui/mici/widgets/pairing_dialog.py similarity index 100% rename from selfdrive/ui/mici/widgets/pairing_dialog.py rename to openpilot/selfdrive/ui/mici/widgets/pairing_dialog.py diff --git a/selfdrive/ui/mici/layouts/__init__.py b/openpilot/selfdrive/ui/onroad/__init__.py similarity index 100% rename from selfdrive/ui/mici/layouts/__init__.py rename to openpilot/selfdrive/ui/onroad/__init__.py diff --git a/selfdrive/ui/onroad/alert_renderer.py b/openpilot/selfdrive/ui/onroad/alert_renderer.py similarity index 98% rename from selfdrive/ui/onroad/alert_renderer.py rename to openpilot/selfdrive/ui/onroad/alert_renderer.py index 6e79d23253..b77b1539f4 100644 --- a/selfdrive/ui/onroad/alert_renderer.py +++ b/openpilot/selfdrive/ui/onroad/alert_renderer.py @@ -1,9 +1,9 @@ import time import pyray as rl from dataclasses import dataclass -from cereal import messaging, log +from openpilot.cereal import messaging, log from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI 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 diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/onroad/augmented_road_view.py similarity index 97% rename from selfdrive/ui/onroad/augmented_road_view.py rename to openpilot/selfdrive/ui/onroad/augmented_road_view.py index 21511412e6..c255804f2d 100644 --- a/selfdrive/ui/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/onroad/augmented_road_view.py @@ -1,6 +1,6 @@ import numpy as np import pyray as rl -from cereal import log +from openpilot.cereal import log from msgq.visionipc import VisionStreamType from openpilot.selfdrive.ui import UI_BORDER_SIZE from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus @@ -85,7 +85,7 @@ class AugmentedRoadView(CameraView, AugmentedRoadViewSP): ) # Render the base camera view - super()._render(rect) + super()._render(self._content_rect) # Draw all UI overlays self.model_renderer.render(self._content_rect) @@ -185,10 +185,13 @@ class AugmentedRoadView(CameraView, AugmentedRoadViewSP): w, h = self._content_rect.width, self._content_rect.height cx, cy = intrinsic[0, 2], intrinsic[1, 2] + # Ensure zoom views the whole area + zoom = max(zoom, w / (2 * cx), h / (2 * cy)) + # Calculate max allowed offsets with margins margin = 5 - max_x_offset = cx * zoom - w / 2 - margin - max_y_offset = cy * zoom - h / 2 - margin + max_x_offset = max(0.0, cx * zoom - w / 2 - margin) + max_y_offset = max(0.0, cy * zoom - h / 2 - margin) # Calculate and clamp offsets to prevent out-of-bounds issues try: diff --git a/selfdrive/ui/onroad/cameraview.py b/openpilot/selfdrive/ui/onroad/cameraview.py similarity index 99% rename from selfdrive/ui/onroad/cameraview.py rename to openpilot/selfdrive/ui/onroad/cameraview.py index 53c2ff8b0b..846bf20bbc 100644 --- a/selfdrive/ui/onroad/cameraview.py +++ b/openpilot/selfdrive/ui/onroad/cameraview.py @@ -4,7 +4,7 @@ import pyray as rl from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage from openpilot.system.ui.widgets import Widget diff --git a/selfdrive/ui/onroad/driver_camera_dialog.py b/openpilot/selfdrive/ui/onroad/driver_camera_dialog.py similarity index 100% rename from selfdrive/ui/onroad/driver_camera_dialog.py rename to openpilot/selfdrive/ui/onroad/driver_camera_dialog.py diff --git a/selfdrive/ui/onroad/driver_state.py b/openpilot/selfdrive/ui/onroad/driver_state.py similarity index 99% rename from selfdrive/ui/onroad/driver_state.py rename to openpilot/selfdrive/ui/onroad/driver_state.py index 5b09e471b5..6b11f0b10b 100644 --- a/selfdrive/ui/onroad/driver_state.py +++ b/openpilot/selfdrive/ui/onroad/driver_state.py @@ -1,6 +1,6 @@ import numpy as np import pyray as rl -from cereal import log +from openpilot.cereal import log from dataclasses import dataclass from openpilot.selfdrive.ui import UI_BORDER_SIZE from openpilot.selfdrive.ui.ui_state import ui_state diff --git a/selfdrive/ui/onroad/exp_button.py b/openpilot/selfdrive/ui/onroad/exp_button.py similarity index 100% rename from selfdrive/ui/onroad/exp_button.py rename to openpilot/selfdrive/ui/onroad/exp_button.py diff --git a/selfdrive/ui/onroad/hud_renderer.py b/openpilot/selfdrive/ui/onroad/hud_renderer.py similarity index 100% rename from selfdrive/ui/onroad/hud_renderer.py rename to openpilot/selfdrive/ui/onroad/hud_renderer.py diff --git a/selfdrive/ui/onroad/model_renderer.py b/openpilot/selfdrive/ui/onroad/model_renderer.py similarity index 98% rename from selfdrive/ui/onroad/model_renderer.py rename to openpilot/selfdrive/ui/onroad/model_renderer.py index 353cc5aa40..a4d9b8b65b 100644 --- a/selfdrive/ui/onroad/model_renderer.py +++ b/openpilot/selfdrive/ui/onroad/model_renderer.py @@ -1,7 +1,8 @@ import colorsys import numpy as np import pyray as rl -from cereal import messaging, car +from openpilot.cereal import messaging +from opendbc.car.structs import car from dataclasses import dataclass, field from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params @@ -38,8 +39,8 @@ class ModelPoints: @dataclass class LeadVehicle: - glow: list[float] = field(default_factory=list) - chevron: list[float] = field(default_factory=list) + glow: list[tuple[float, float]] = field(default_factory=list) + chevron: list[tuple[float, float]] = field(default_factory=list) fill_alpha: int = 0 @@ -159,7 +160,7 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP): leads = [radar_state.leadOne, radar_state.leadTwo] for i, lead_data in enumerate(leads): - if lead_data and lead_data.status: + if lead_data and lead_data.present: d_rel, y_rel, v_rel = lead_data.dRel, lead_data.yRel, lead_data.vRel idx = self._get_path_length_idx(path_x_array, d_rel) @@ -185,7 +186,7 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP): road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, 0.025, 0.0, max_idx, max_distance) # Update path using raw points - if lead and lead.status: + if lead and lead.present: lead_d = lead.dRel * 2.0 max_distance = np.clip(lead_d - min(lead_d * 0.35, 10.0), 0.0, max_distance) diff --git a/selfdrive/ui/soundd.py b/openpilot/selfdrive/ui/soundd.py similarity index 78% rename from selfdrive/ui/soundd.py rename to openpilot/selfdrive/ui/soundd.py index 6b64289766..a0ffbacffe 100644 --- a/selfdrive/ui/soundd.py +++ b/openpilot/selfdrive/ui/soundd.py @@ -4,7 +4,7 @@ import time import wave -from cereal import car, messaging, custom +from openpilot.cereal import log, messaging, custom from openpilot.common.basedir import BASEDIR from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.realtime import Ratekeeper @@ -12,7 +12,7 @@ from openpilot.common.utils import retry from openpilot.common.swaglog import cloudlog from openpilot.system import micd -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.sunnypilot.selfdrive.ui.quiet_mode import QuietMode @@ -24,7 +24,7 @@ ALERT_RAMP_TIME = 4 # seconds to ramp to max volume for warningImmediate SELFDRIVE_STATE_TIMEOUT = 5 # 5 seconds FILTER_DT = 1. / (micd.SAMPLE_RATE / micd.FFT_SAMPLES) -AMBIENT_DB = 24 # DB where MIN_VOLUME is applied +AMBIENT_DB = 26 # DB where MIN_VOLUME is applied DB_SCALE = 30 # AMBIENT_DB + DB_SCALE is where MAX_VOLUME is applied VOLUME_BASE = 20 @@ -32,7 +32,7 @@ if HARDWARE.get_device_type() == "tizi": AMBIENT_DB = 30 VOLUME_BASE = 10 -AudibleAlert = car.CarControl.HUDControl.AudibleAlert +AudibleAlert = log.SelfdriveState.AudibleAlert AudibleAlertSP = custom.SelfdriveStateSP.AudibleAlert @@ -48,12 +48,15 @@ sound_list: dict[int, tuple[str, int | None, float]] = { AudibleAlert.disengage: ("disengage.wav", 1, MAX_VOLUME), AudibleAlert.refuse: ("refuse.wav", 1, MAX_VOLUME), - AudibleAlert.prompt: ("prompt.wav", 1, MAX_VOLUME), - AudibleAlert.promptRepeat: ("prompt.wav", None, MAX_VOLUME), - AudibleAlert.promptDistracted: ("prompt_distracted.wav", None, MAX_VOLUME), + AudibleAlert.prompt: ("warning.wav", 1, MAX_VOLUME), + AudibleAlert.promptRepeat: ("warning.wav", None, MAX_VOLUME), + AudibleAlert.promptDistracted: ("dm_warning.wav", None, MAX_VOLUME), - AudibleAlert.warningSoft: ("warning_soft.wav", None, MAX_VOLUME), - AudibleAlert.warningImmediate: ("warning_immediate.wav", None, MAX_VOLUME), + AudibleAlert.preAlert: ("pre_alert.wav", 1, MAX_VOLUME), + AudibleAlert.complete: ("complete.wav", 1, MAX_VOLUME), + + AudibleAlert.warningSoft: ("critical.wav", None, MAX_VOLUME), + AudibleAlert.warningImmediate: ("dm_critical.wav", None, MAX_VOLUME), **sound_list_sp, } @@ -87,6 +90,7 @@ class Soundd(QuietMode): self.ramp_start_time = 0. self.selfdrive_timeout_alert = False + self.pending_stop = False self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False) @@ -97,7 +101,7 @@ class Soundd(QuietMode): for sound in sound_list: filename, play_count, volume = sound_list[sound] - with wave.open(BASEDIR + "/selfdrive/assets/sounds/" + filename, 'r') as wavefile: + with wave.open(BASEDIR + "/openpilot/selfdrive/assets/sounds/" + filename, 'r') as wavefile: assert wavefile.getnchannels() == 1 assert wavefile.getsampwidth() == 2 assert wavefile.getframerate() == SAMPLE_RATE @@ -123,6 +127,12 @@ class Soundd(QuietMode): ret[written_frames:written_frames+frames_to_write] = sound_data[current_sound_frame:current_sound_frame+frames_to_write] written_frames += frames_to_write self.current_sound_frame += frames_to_write + current_sound_frame = self.current_sound_frame % len(sound_data) + loops = self.current_sound_frame // len(sound_data) + if self.pending_stop and current_sound_frame == 0: + self.current_alert = AudibleAlert.none + self.pending_stop = False + break return ret * self.current_volume @@ -132,7 +142,16 @@ class Soundd(QuietMode): data_out[:frames, 0] = self.get_sound_data(frames) def update_alert(self, new_alert): - current_alert_played_once = self.current_alert == AudibleAlert.none or self.current_sound_frame > len(self.loaded_sounds[self.current_alert]) + current_alert_played_once = self.current_alert == AudibleAlert.none or self.current_sound_frame >= len(self.loaded_sounds[self.current_alert]) + # let looping sounds finish the current loop instead of cutting off mid tone + if new_alert == AudibleAlert.none and self.current_alert != AudibleAlert.none and sound_list[self.current_alert][1] is None: + if current_alert_played_once: + self.pending_stop = True + else: + self.current_alert = AudibleAlert.none + self.current_sound_frame = 0 + return + self.pending_stop = False if self.current_alert != new_alert and (new_alert != AudibleAlert.none or current_alert_played_once): if new_alert == AudibleAlert.warningImmediate: self.ramp_start_volume = self.current_volume @@ -177,10 +196,11 @@ class Soundd(QuietMode): self.load_param() - # Always update volume, even when alert is playing + # freeze volume during alerts to avoid mic feedback increasing volume if sm.updated['soundPressure']: self.spl_filter_weighted.update(sm["soundPressure"].soundPressureWeightedDb) - self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x)) + if self.current_alert == AudibleAlert.none: + self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x)) self.get_audible_alert(sm) diff --git a/selfdrive/ui/onroad/__init__.py b/openpilot/selfdrive/ui/sunnypilot/__init__.py similarity index 100% rename from selfdrive/ui/onroad/__init__.py rename to openpilot/selfdrive/ui/sunnypilot/__init__.py diff --git a/selfdrive/ui/sunnypilot/__init__.py b/openpilot/selfdrive/ui/sunnypilot/layouts/__init__.py similarity index 100% rename from selfdrive/ui/sunnypilot/__init__.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/__init__.py diff --git a/selfdrive/ui/sunnypilot/layouts/onboarding.py b/openpilot/selfdrive/ui/sunnypilot/layouts/onboarding.py similarity index 98% rename from selfdrive/ui/sunnypilot/layouts/onboarding.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/onboarding.py index 7e532678b0..a86677a7b1 100644 --- a/selfdrive/ui/sunnypilot/layouts/onboarding.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/onboarding.py @@ -11,7 +11,7 @@ from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.label import Label -from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined +from openpilot.common.version import sunnylink_consent_version, sunnylink_consent_declined class SunnylinkConsentPage(Widget): diff --git a/selfdrive/ui/sunnypilot/layouts/__init__.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/__init__.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/__init__.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/__init__.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/cruise.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/cruise.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/cruise.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_policy.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_policy.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_policy.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_policy.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_settings.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_settings.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_settings.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_settings.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/developer.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/developer.py similarity index 98% rename from selfdrive/ui/sunnypilot/layouts/settings/developer.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/developer.py index 4cac66e316..a8768af5be 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/developer.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/developer.py @@ -10,8 +10,8 @@ from pathlib import Path from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout -from openpilot.system.hardware import PC -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware import PC +from openpilot.common.hardware.hw import Paths from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import DialogResult diff --git a/selfdrive/ui/sunnypilot/layouts/settings/device.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/device.py similarity index 99% rename from selfdrive/ui/sunnypilot/layouts/settings/device.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/device.py index 1fb9314739..e5c956cd81 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/device.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/device.py @@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details. from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp, multiple_button_item_sp, button_item_sp, \ diff --git a/selfdrive/ui/sunnypilot/layouts/settings/display.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/display.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/models.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py similarity index 93% rename from selfdrive/ui/sunnypilot/layouts/settings/models.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py index 587c3560c9..be2883d718 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -9,7 +9,7 @@ import re import time import pyray as rl -from cereal import custom +from openpilot.cereal import custom from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL from openpilot.common.constants import CV from openpilot.selfdrive.ui.ui_state import device, ui_state @@ -43,7 +43,7 @@ class ModelsLayout(Widget): self._initialize_items() self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB") - for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay")]: + for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay"), (self.camera_offset, "CameraOffset")]: ctrl.action_item.set_value(int(float(ui_state.params.get(key, return_default=True)) * 100)) self._scroller = Scroller(self.items, line_separator=True, spacing=0) @@ -93,9 +93,14 @@ class ModelsLayout(Widget): self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle") + self.camera_offset = option_item_sp(tr("Adjust Camera Offset"), "CameraOffset", -35, 35, + tr("Virtually shift camera's perspective to move model's center to Left(+ values) or Right (- values)"), + 1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True, + lambda v: f"{v / 100:.2f} m") + self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label, - self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item, self.lane_turn_desire_toggle, - self.lane_turn_value_control, self.lagd_toggle, self.delay_control] + self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item, + self.lane_turn_desire_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control, self.camera_offset] def _update_lagd_description(self, lagd_toggle: bool): desc = tr("Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. " + @@ -232,14 +237,16 @@ class ModelsLayout(Widget): advanced_controls: bool = ui_state.params.get_bool("ShowAdvancedControls") turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire") live_delay: bool = ui_state.params.get_bool("LagdToggle") + camera_offset: bool = ui_state.params.get("ModelManager_ActiveBundle") is not None self.lane_turn_desire_toggle.action_item.set_state(turn_desire) self.lane_turn_value_control.set_visible(turn_desire and advanced_controls) self.lagd_toggle.action_item.set_state(live_delay) self.delay_control.set_visible(not live_delay and advanced_controls) new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100 - if self.lane_turn_value_control.action_item.value_change_step != new_step: + if self.lane_turn_value_control.action_item is not None and self.lane_turn_value_control.action_item.value_change_step != new_step: self.lane_turn_value_control.action_item.value_change_step = new_step + self.camera_offset.set_visible(camera_offset) self._update_lagd_description(live_delay) self.model_manager = ui_state.sm["modelManagerSP"] diff --git a/selfdrive/ui/sunnypilot/layouts/settings/navigation.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/navigation.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/navigation.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/navigation.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/network.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/network.py similarity index 95% rename from selfdrive/ui/sunnypilot/layouts/settings/network.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/network.py index 14f573c628..2663607fc8 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/network.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/network.py @@ -38,8 +38,8 @@ class NetworkUISP(NetworkUI): self.scan_button.set_text(tr("Scan")) self.scan_button.set_enabled(True) - def _render(self, rect: rl.Rectangle): - super()._render(rect) + def _render(self, _): + super()._render(_) if self._current_panel == PanelType.WIFI: self.scan_button.set_position(self._rect.x, self._rect.y + 20) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/osm.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/osm.py similarity index 99% rename from selfdrive/ui/sunnypilot/layouts/settings/osm.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/osm.py index 58236961b9..7b30e880f9 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/osm.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/osm.py @@ -16,7 +16,7 @@ from time import monotonic from openpilot.common.params import Params from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.selfdrive.ui.layouts.settings.software import time_ago -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import DialogResult, Widget diff --git a/selfdrive/ui/sunnypilot/layouts/settings/settings.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/settings.py similarity index 98% rename from selfdrive/ui/sunnypilot/layouts/settings/settings.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/settings.py index 4917c9a157..1b85c0923a 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/settings.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/settings.py @@ -37,7 +37,7 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller OP.PANEL_COLOR = rl.Color(10, 10, 10, 255) ICON_SIZE = 70 -OP.PanelType = IntEnum( +OP.PanelType = IntEnum( # type: ignore[assignment] # ty: ignore[invalid-assignment] "PanelType", [es.name for es in OP.PanelType] + [ "SUNNYLINK", @@ -180,20 +180,18 @@ class SettingsLayoutSP(OP.SettingsLayout): self._sidebar_scroller.render(nav_rect) return - def _handle_mouse_release(self, mouse_pos: MousePos) -> bool: + def _handle_mouse_release(self, mouse_pos: MousePos) -> None: # Check close button if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect): if self._close_callback: self._close_callback() - return True + return # Check navigation buttons for panel_type, panel_info in self._panels.items(): if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect) and self._sidebar_scroller.scroll_panel.is_touch_valid(): self.set_current_panel(panel_type) - return True - - return False + return def show_event(self): super().show_event() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/software.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/software.py similarity index 96% rename from selfdrive/ui/sunnypilot/layouts/settings/software.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/software.py index 7765a58b65..d067f1330d 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/software.py @@ -4,11 +4,11 @@ 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 os +import subprocess from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.widgets import DialogResult @@ -78,7 +78,7 @@ class SoftwareLayoutSP(SoftwareLayout): if selection: ui_state.params.put("UpdaterTargetBranch", selection) self._branch_btn.action_item.set_value(selection) - os.system("pkill -SIGUSR1 -f system.updated.updated") + subprocess.run(["pkill", "-SIGUSR1", "-f", "openpilot.system.updated.updated"], check=False) self._branch_dialog = None self._branch_dialog = TreeOptionDialog(tr("Select a branch"), folders, current_target, "", diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering.py similarity index 96% rename from selfdrive/ui/sunnypilot/layouts/settings/steering.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering.py index e31ee1891c..15cb6a15e0 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/steering.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering.py @@ -4,7 +4,7 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import car +from opendbc.car.structs import car from enum import IntEnum from openpilot.selfdrive.ui.ui_state import ui_state @@ -134,6 +134,11 @@ class SteeringLayout(Widget): enforce_torque_enabled = self._torque_control_toggle.action_item.get_state() nnlc_enabled = self._nnlc_toggle.action_item.get_state() + if enforce_torque_enabled and nnlc_enabled: + self._torque_control_toggle.action_item.set_state(False) + self._nnlc_toggle.action_item.set_state(False) + enforce_torque_enabled = False + nnlc_enabled = False self._nnlc_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not enforce_torque_enabled) self._torque_control_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not nnlc_enabled) self._torque_customization_button.action_item.set_enabled(self._torque_control_toggle.action_item.get_state()) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py similarity index 98% rename from selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py index d4976b1295..f3c4419e45 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py @@ -21,7 +21,7 @@ from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.network import NavButton from openpilot.system.ui.widgets.scroller_tici import Scroller -TORQUE_VERSIONS_PATH = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json") +TORQUE_VERSIONS_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json") class TorqueSettingsLayout(Widget): diff --git a/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py similarity index 99% rename from selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py index 1d9b99d5fd..5e75e797cb 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py @@ -5,7 +5,7 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ import pyray as rl -from cereal import custom +from openpilot.cereal import custom from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkConsentPage from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID @@ -20,7 +20,7 @@ from openpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDial from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.list_view import dual_button_item from openpilot.system.ui.widgets.scroller_tici import Scroller, LineSeparator -from openpilot.system.version import sunnylink_consent_version +from openpilot.common.version import sunnylink_consent_version class SunnylinkHeader(Widget): diff --git a/selfdrive/ui/sunnypilot/layouts/settings/trips.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/trips.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/trips.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/trips.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/__init__.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/__init__.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/__init__.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/__init__.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/base.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/base.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/base.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/base.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/body.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/body.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/body.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/body.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/chrysler.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/chrysler.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/chrysler.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/chrysler.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/factory.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/factory.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/factory.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/factory.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/ford.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/ford.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/ford.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/ford.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/gm.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/gm.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/gm.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/gm.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/honda.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/honda.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/honda.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/honda.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/hyundai.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/hyundai.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/hyundai.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/hyundai.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/mazda.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/mazda.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/mazda.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/mazda.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/nissan.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/nissan.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/nissan.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/nissan.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/psa.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/psa.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/psa.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/psa.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/rivian.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/rivian.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/rivian.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/rivian.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/subaru.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/subaru.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/subaru.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/subaru.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/tesla.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/tesla.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/tesla.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/tesla.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/toyota.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/toyota.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/toyota.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/toyota.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/volkswagen.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/volkswagen.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/volkswagen.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/volkswagen.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/platform_selector.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/platform_selector.py similarity index 98% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/platform_selector.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/platform_selector.py index 6102e8e9b7..aab935d4b8 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/platform_selector.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/platform_selector.py @@ -21,7 +21,7 @@ from openpilot.system.ui.sunnypilot.lib.styles import style from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder from openpilot.selfdrive.ui.ui_state import ui_state -CAR_LIST_JSON_OUT = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "car", "car_list.json") +CAR_LIST_JSON_OUT = os.path.join(BASEDIR, "openpilot", "sunnypilot", "selfdrive", "car", "car_list.json") class LegendWidget(Widget): diff --git a/selfdrive/ui/sunnypilot/layouts/settings/visuals.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/visuals.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/settings/visuals.py diff --git a/selfdrive/ui/sunnypilot/layouts/sidebar.py b/openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/sidebar.py rename to openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/__init__.py b/openpilot/selfdrive/ui/sunnypilot/mici/__init__.py similarity index 100% rename from selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/__init__.py rename to openpilot/selfdrive/ui/sunnypilot/mici/__init__.py diff --git a/selfdrive/ui/sunnypilot/mici/__init__.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/__init__.py similarity index 100% rename from selfdrive/ui/sunnypilot/mici/__init__.py rename to openpilot/selfdrive/ui/sunnypilot/mici/layouts/__init__.py diff --git a/selfdrive/ui/sunnypilot/mici/layouts/models.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py similarity index 98% rename from selfdrive/ui/sunnypilot/mici/layouts/models.py rename to openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py index 138ff87b75..8999281430 100644 --- a/selfdrive/ui/sunnypilot/mici/layouts/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py @@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details. from collections.abc import Callable import pyray as rl -from cereal import custom +from openpilot.cereal import custom from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout @@ -138,7 +138,7 @@ class ModelsLayoutMici(NavScroller): self._show_selection_view(btns, self._show_folders) def _reset_main_view(self): - self._scroller._items = self.main_items + self._scroller._items = self.main_items # type: ignore[assignment] # ty: ignore[invalid-assignment] self.set_back_callback(self.original_back_callback) self._scroller.scroll_panel.set_offset(0) self._scroller.scroll_to(0) diff --git a/selfdrive/ui/sunnypilot/mici/layouts/onboarding.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/onboarding.py similarity index 92% rename from selfdrive/ui/sunnypilot/mici/layouts/onboarding.py rename to openpilot/selfdrive/ui/sunnypilot/mici/layouts/onboarding.py index a98f5a2e2e..5aed8fac38 100644 --- a/selfdrive/ui/sunnypilot/mici/layouts/onboarding.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/onboarding.py @@ -16,6 +16,9 @@ class SunnylinkConsentPage(NavScroller): def __init__(self, on_accept: Callable | None = None, on_decline: Callable | None = None): super().__init__() + assert on_accept is not None and callable(on_accept) + assert on_decline is not None and callable(on_decline) + self._accept_button = BigConfirmationCircleButton("enable\nsunnylink", gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64), on_accept, exit_on_confirm=False) diff --git a/selfdrive/ui/sunnypilot/mici/layouts/settings.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/settings.py similarity index 100% rename from selfdrive/ui/sunnypilot/mici/layouts/settings.py rename to openpilot/selfdrive/ui/sunnypilot/mici/layouts/settings.py diff --git a/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py similarity index 99% rename from selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py rename to openpilot/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py index 85ebf55f46..e804c78035 100644 --- a/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py @@ -8,7 +8,7 @@ import pyray as rl from collections.abc import Callable -from cereal import custom +from openpilot.cereal import custom from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkConsentPage @@ -20,7 +20,7 @@ from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.scroller import NavScroller -from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined +from openpilot.common.version import sunnylink_consent_version, sunnylink_consent_declined class SunnylinkInfo(Widget): def __init__(self): diff --git a/selfdrive/ui/sunnypilot/mici/layouts/__init__.py b/openpilot/selfdrive/ui/sunnypilot/mici/onroad/__init__.py similarity index 100% rename from selfdrive/ui/sunnypilot/mici/layouts/__init__.py rename to openpilot/selfdrive/ui/sunnypilot/mici/onroad/__init__.py diff --git a/selfdrive/ui/sunnypilot/mici/onroad/confidence_ball.py b/openpilot/selfdrive/ui/sunnypilot/mici/onroad/confidence_ball.py similarity index 100% rename from selfdrive/ui/sunnypilot/mici/onroad/confidence_ball.py rename to openpilot/selfdrive/ui/sunnypilot/mici/onroad/confidence_ball.py diff --git a/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py similarity index 100% rename from selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py rename to openpilot/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py diff --git a/selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py b/openpilot/selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py similarity index 100% rename from selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py rename to openpilot/selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py diff --git a/selfdrive/ui/sunnypilot/mici/onroad/__init__.py b/openpilot/selfdrive/ui/sunnypilot/mici/widgets/__init__.py similarity index 100% rename from selfdrive/ui/sunnypilot/mici/onroad/__init__.py rename to openpilot/selfdrive/ui/sunnypilot/mici/widgets/__init__.py diff --git a/selfdrive/ui/sunnypilot/mici/widgets/sunnylink_pairing_dialog.py b/openpilot/selfdrive/ui/sunnypilot/mici/widgets/sunnylink_pairing_dialog.py similarity index 100% rename from selfdrive/ui/sunnypilot/mici/widgets/sunnylink_pairing_dialog.py rename to openpilot/selfdrive/ui/sunnypilot/mici/widgets/sunnylink_pairing_dialog.py diff --git a/selfdrive/ui/sunnypilot/mici/widgets/__init__.py b/openpilot/selfdrive/ui/sunnypilot/onroad/__init__.py similarity index 100% rename from selfdrive/ui/sunnypilot/mici/widgets/__init__.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/__init__.py diff --git a/selfdrive/ui/sunnypilot/onroad/alert_renderer.py b/openpilot/selfdrive/ui/sunnypilot/onroad/alert_renderer.py similarity index 100% rename from selfdrive/ui/sunnypilot/onroad/alert_renderer.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/alert_renderer.py diff --git a/selfdrive/ui/sunnypilot/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/sunnypilot/onroad/augmented_road_view.py similarity index 100% rename from selfdrive/ui/sunnypilot/onroad/augmented_road_view.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/augmented_road_view.py diff --git a/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py b/openpilot/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py similarity index 100% rename from selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py diff --git a/selfdrive/ui/sunnypilot/onroad/chevron_metrics.py b/openpilot/selfdrive/ui/sunnypilot/onroad/chevron_metrics.py similarity index 97% rename from selfdrive/ui/sunnypilot/onroad/chevron_metrics.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/chevron_metrics.py index a8a342c129..1dded3fc62 100644 --- a/selfdrive/ui/sunnypilot/onroad/chevron_metrics.py +++ b/openpilot/selfdrive/ui/sunnypilot/onroad/chevron_metrics.py @@ -128,8 +128,8 @@ class ChevronMetrics: lead_one = radar_state.leadOne lead_two = radar_state.leadTwo - has_lead_one = lead_one.status if lead_one else False - has_lead_two = lead_two.status if lead_two else False + has_lead_one = lead_one.present if lead_one else False + has_lead_two = lead_two.present if lead_two else False self.update_alpha(has_lead_one or has_lead_two) diff --git a/selfdrive/ui/sunnypilot/onroad/circular_alerts.py b/openpilot/selfdrive/ui/sunnypilot/onroad/circular_alerts.py similarity index 99% rename from selfdrive/ui/sunnypilot/onroad/circular_alerts.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/circular_alerts.py index f90fc81914..c44e4455eb 100644 --- a/selfdrive/ui/sunnypilot/onroad/circular_alerts.py +++ b/openpilot/selfdrive/ui/sunnypilot/onroad/circular_alerts.py @@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details. """ import pyray as rl -from cereal import log +from openpilot.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 DeveloperUiState diff --git a/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py b/openpilot/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py similarity index 100% rename from selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py diff --git a/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py b/openpilot/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py similarity index 99% rename from selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py index 389692d30b..4119990f22 100644 --- a/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py +++ b/openpilot/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py @@ -42,7 +42,7 @@ class LeadInfoElement: @staticmethod def get_lead_status(sm): lead_one = sm['radarState'].leadOne - return lead_one.status, lead_one.dRel, lead_one.vRel + return lead_one.present, lead_one.dRel, lead_one.vRel @staticmethod def get_lead_color(lead_d_rel: float, lead_v_rel: float = 0.0, use_v_rel: bool = False) -> rl.Color: diff --git a/selfdrive/ui/sunnypilot/onroad/driver_state.py b/openpilot/selfdrive/ui/sunnypilot/onroad/driver_state.py similarity index 100% rename from selfdrive/ui/sunnypilot/onroad/driver_state.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/driver_state.py diff --git a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py b/openpilot/selfdrive/ui/sunnypilot/onroad/hud_renderer.py similarity index 100% rename from selfdrive/ui/sunnypilot/onroad/hud_renderer.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/hud_renderer.py diff --git a/selfdrive/ui/sunnypilot/onroad/model_renderer.py b/openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py similarity index 100% rename from selfdrive/ui/sunnypilot/onroad/model_renderer.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py diff --git a/selfdrive/ui/sunnypilot/onroad/rainbow_path.py b/openpilot/selfdrive/ui/sunnypilot/onroad/rainbow_path.py similarity index 100% rename from selfdrive/ui/sunnypilot/onroad/rainbow_path.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/rainbow_path.py diff --git a/selfdrive/ui/sunnypilot/onroad/road_name.py b/openpilot/selfdrive/ui/sunnypilot/onroad/road_name.py similarity index 100% rename from selfdrive/ui/sunnypilot/onroad/road_name.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/road_name.py diff --git a/selfdrive/ui/sunnypilot/onroad/rocket_fuel.py b/openpilot/selfdrive/ui/sunnypilot/onroad/rocket_fuel.py similarity index 100% rename from selfdrive/ui/sunnypilot/onroad/rocket_fuel.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/rocket_fuel.py diff --git a/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py b/openpilot/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py similarity index 100% rename from selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py diff --git a/selfdrive/ui/sunnypilot/onroad/speed_limit.py b/openpilot/selfdrive/ui/sunnypilot/onroad/speed_limit.py similarity index 98% rename from selfdrive/ui/sunnypilot/onroad/speed_limit.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/speed_limit.py index 346262b86c..fa8143eb32 100644 --- a/selfdrive/ui/sunnypilot/onroad/speed_limit.py +++ b/openpilot/selfdrive/ui/sunnypilot/onroad/speed_limit.py @@ -9,12 +9,12 @@ from dataclasses import dataclass from enum import StrEnum import pyray as rl -from cereal import custom +from openpilot.cereal import custom from openpilot.common.constants import CV from openpilot.selfdrive.ui.onroad.hud_renderer import UI_CONFIG from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE 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 @@ -198,7 +198,7 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer): self._draw_ahead_info(sign_rect) def _draw_sign_main(self, rect, alpha=1.0): - speed_limit_warning_enabled = ui_state.speed_limit_mode >= SpeedLimitMode.warning + speed_limit_warning_enabled = ui_state.speed_limit_mode is not None and ui_state.speed_limit_mode >= SpeedLimitMode.warning has_limit = self.speed_limit_valid or self.speed_limit_last_valid is_overspeed = has_limit and round(self.speed_limit_final_last) < round(self.speed) diff --git a/selfdrive/ui/sunnypilot/onroad/speed_renderer.py b/openpilot/selfdrive/ui/sunnypilot/onroad/speed_renderer.py similarity index 100% rename from selfdrive/ui/sunnypilot/onroad/speed_renderer.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/speed_renderer.py diff --git a/selfdrive/ui/sunnypilot/onroad/turn_signal.py b/openpilot/selfdrive/ui/sunnypilot/onroad/turn_signal.py similarity index 100% rename from selfdrive/ui/sunnypilot/onroad/turn_signal.py rename to openpilot/selfdrive/ui/sunnypilot/onroad/turn_signal.py diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/openpilot/selfdrive/ui/sunnypilot/ui_state.py similarity index 96% rename from selfdrive/ui/sunnypilot/ui_state.py rename to openpilot/selfdrive/ui/sunnypilot/ui_state.py index 8cd37583f3..c2729bbd91 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/openpilot/selfdrive/ui/sunnypilot/ui_state.py @@ -6,7 +6,8 @@ See the LICENSE.md file in the root directory for more details. """ from enum import Enum -from cereal import messaging, log, car, custom +from openpilot.cereal import messaging, log, custom +from opendbc.car.structs import car from openpilot.common.params import Params from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState @@ -179,6 +180,10 @@ class UIStateSP: CP = self.CP if CP is not None: + if self.params.get_bool("EnforceTorqueControl") and self.params.get_bool("NeuralNetworkLateralControl"): + self.params.put_bool("EnforceTorqueControl", False, block=True) + self.params.put_bool("NeuralNetworkLateralControl", False, block=True) + # Angle steering: no torque-based lateral controls if CP.steerControlType == car.CarParams.SteerControlType.angle: self.params.remove("EnforceTorqueControl") @@ -219,8 +224,7 @@ class UIStateSP: class DeviceSP: - @staticmethod - def _set_awake(on: bool, _ui_state): + def _set_awake(self, on: bool, _ui_state=None): if _ui_state.boot_offroad_mode == 1 and not on: _ui_state.params.put_bool("OffroadMode", True) diff --git a/selfdrive/ui/sunnypilot/onroad/__init__.py b/openpilot/selfdrive/ui/tests/__init__.py similarity index 100% rename from selfdrive/ui/sunnypilot/onroad/__init__.py rename to openpilot/selfdrive/ui/tests/__init__.py diff --git a/selfdrive/ui/tests/body.py b/openpilot/selfdrive/ui/tests/body.py similarity index 92% rename from selfdrive/ui/tests/body.py rename to openpilot/selfdrive/ui/tests/body.py index 07a2ef5128..b9f5c2d18c 100755 --- a/selfdrive/ui/tests/body.py +++ b/openpilot/selfdrive/ui/tests/body.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import time -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging if __name__ == "__main__": while True: diff --git a/selfdrive/ui/tests/cycle_offroad_alerts.py b/openpilot/selfdrive/ui/tests/cycle_offroad_alerts.py similarity index 90% rename from selfdrive/ui/tests/cycle_offroad_alerts.py rename to openpilot/selfdrive/ui/tests/cycle_offroad_alerts.py index fcb4a72c77..c2690edf7d 100755 --- a/selfdrive/ui/tests/cycle_offroad_alerts.py +++ b/openpilot/selfdrive/ui/tests/cycle_offroad_alerts.py @@ -12,7 +12,7 @@ from openpilot.system.updated.updated import parse_release_notes if __name__ == "__main__": params = Params() - with open(os.path.join(BASEDIR, "selfdrive/selfdrived/alerts_offroad.json")) as f: + with open(os.path.join(BASEDIR, "openpilot/selfdrive/selfdrived/alerts_offroad.json")) as f: offroad_alerts = json.load(f) t = 10 if len(sys.argv) < 2 else int(sys.argv[1]) diff --git a/openpilot/selfdrive/ui/tests/diff/diff.py b/openpilot/selfdrive/ui/tests/diff/diff.py new file mode 100755 index 0000000000..9dd7c3bc0e --- /dev/null +++ b/openpilot/selfdrive/ui/tests/diff/diff.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +import os +import sys +import subprocess +import webbrowser +import argparse +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from openpilot.common.basedir import BASEDIR + +DIFF_OUT_DIR = Path(BASEDIR) / "openpilot" / "selfdrive" / "ui" / "tests" / "diff" / "report" +HTML_TEMPLATE_PATH = Path(__file__).with_name("diff_template.html") + + +def extract_framehashes(video_path: Path) -> list[str]: + cmd = ['ffmpeg', '-nostdin', '-i', video_path, '-map', '0:v:0', '-vsync', '0', '-f', 'framehash', '-hash', 'md5', '-'] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + hashes = [] + for line in result.stdout.splitlines(): + if not line or line.startswith('#'): + continue + parts = line.split(',') + if len(parts) < 4: + continue + hashes.append(parts[-1].strip()) + return hashes + + +def get_video_frame_hashes(video1: Path, video2: Path) -> tuple[list[str], list[str]]: + """Hash every frame of both videos in parallel and return the two hash lists.""" + with ThreadPoolExecutor(max_workers=2) as executor: + print("Generating frame hashes for both videos...") + future1 = executor.submit(extract_framehashes, video1) + future2 = executor.submit(extract_framehashes, video2) + hashes1 = future1.result() + hashes2 = future2.result() + return hashes1, hashes2 + + +def create_diff_video(video1: Path, video2: Path, output: Path) -> None: + """Create a diff video of two clips using ffmpeg blend filter with difference mode.""" + cmd = ['ffmpeg', '-nostdin', '-i', video1, '-i', video2, '-filter_complex', 'blend=all_mode=difference', '-vsync', '0', '-y', output] + subprocess.run(cmd, capture_output=True, check=True) + + +def find_frame_differences(hashes1: list[str], hashes2: list[str]) -> list[int]: + """Compare two lists of frame hashes and return the indices of different frames.""" + different_frames = [] + for i, (h1, h2) in enumerate(zip(hashes1, hashes2, strict=False)): + if h1 != h2: + different_frames.append(i) + return different_frames + + +def generate_html_report(videos: tuple[Path, Path], basedir: str, different_frames: list[int], frame_counts: tuple[int, int], diff_video_name: str) -> str: + total_frames = max(frame_counts) + frame_delta = frame_counts[1] - frame_counts[0] + different_total = len(different_frames) + abs(frame_delta) + + result_text = ( + f"✅ Videos are identical! ({total_frames} frames)" + if different_total == 0 + else f"❌ Found {different_total} different frames out of {total_frames} total ({different_total / total_frames * 100:.1f}%)." + + (f" Video {'2' if frame_delta > 0 else '1'} is longer by {abs(frame_delta)} frames." if frame_delta != 0 else "") + ) + print(f" Results: {result_text}") + + # Load HTML template and replace placeholders + html = HTML_TEMPLATE_PATH.read_text() + placeholders = { + "VIDEO1_SRC": os.path.join(basedir, videos[0].name), + "VIDEO2_SRC": os.path.join(basedir, videos[1].name), + "DIFF_SRC": os.path.join(basedir, diff_video_name), + "RESULT_TEXT": result_text, + } + for key, value in placeholders.items(): + html = html.replace(f"${key}", value) + + return html + + +def main(): + parser = argparse.ArgumentParser(description='Compare two videos and generate HTML diff report') + parser.add_argument('video1', help='First video file') + parser.add_argument('video2', help='Second video file') + parser.add_argument('output', nargs='?', default='diff.html', help='Output HTML file (default: diff.html)') + parser.add_argument("--basedir", type=str, help="Base path for files in HTML report", default="") + parser.add_argument('--no-open', action='store_true', help='Do not open HTML report in browser') + + args = parser.parse_args() + + if not args.output.lower().endswith('.html'): + args.output += '.html' + + video1, video2 = Path(args.video1), Path(args.video2) + missing = [str(p) for p in (video1, video2) if not p.is_file()] + if missing: + parser.error(f"Video file(s) not found: {', '.join(missing)}") + + diff_video_name = f"{Path(args.output).stem}.mp4" # diff video name derived from output HTML name + + os.makedirs(DIFF_OUT_DIR, exist_ok=True) + + print("=" * 60) + print("UI VIDEO DIFF - HTML REPORT") + print("=" * 60) + print(f"Video 1: {video1}") + print(f"Video 2: {video2}") + print(f"HTML output: {args.output}") + print(f"Diff video: {diff_video_name}") + print() + + print("[1/4] Starting full video diff generation in background thread...") + diff_executor = ThreadPoolExecutor(max_workers=1) + diff_future = diff_executor.submit(create_diff_video, video1, video2, DIFF_OUT_DIR / diff_video_name) + + print("[2/4] Hashing frames...") + hashes1, hashes2 = get_video_frame_hashes(video1, video2) + frame_counts = (len(hashes1), len(hashes2)) + print(f" Found {frame_counts[0]} frames in video 1 and {frame_counts[1]} frames in video 2.") + + print("[3/4] Finding different frames...") + different_frames = find_frame_differences(hashes1, hashes2) + print(f" Found {len(different_frames)} different frames.") + + print("[4/4] Generating HTML report...") + html = generate_html_report((video1, video2), args.basedir, different_frames, frame_counts, diff_video_name) + + output_path = DIFF_OUT_DIR / args.output + with open(output_path, 'w') as f: + f.write(html) + + print(f" Report generated at: {output_path}") + + # Open in browser by default + if not args.no_open: + print(f"Opening {args.output} in browser...") + webbrowser.open(f'file://{os.path.abspath(output_path)}') + + # Wait for diff video generation to finish before exiting + if not diff_future.done(): + print("Waiting for diff video generation to finish...") + diff_future.result() + diff_executor.shutdown() + + extra_frames = abs(frame_counts[0] - frame_counts[1]) + return 0 if (len(different_frames) + extra_frames) == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/selfdrive/ui/tests/diff/diff_template.html b/openpilot/selfdrive/ui/tests/diff/diff_template.html similarity index 100% rename from selfdrive/ui/tests/diff/diff_template.html rename to openpilot/selfdrive/ui/tests/diff/diff_template.html diff --git a/selfdrive/ui/tests/diff/replay.py b/openpilot/selfdrive/ui/tests/diff/replay.py similarity index 98% rename from selfdrive/ui/tests/diff/replay.py rename to openpilot/selfdrive/ui/tests/diff/replay.py index cb88ee9da8..e378076850 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/openpilot/selfdrive/ui/tests/diff/replay.py @@ -7,14 +7,14 @@ import pyray as rl from tqdm import tqdm from typing import Literal from collections.abc import Callable -from cereal.messaging import PubMaster +from openpilot.cereal.messaging import PubMaster from openpilot.common.api import Api from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.prefix import OpenpilotPrefix from openpilot.selfdrive.ui.tests.diff.diff import DIFF_OUT_DIR from openpilot.system.updated.updated import parse_release_notes -from openpilot.system.version import terms_version, training_version, terms_version_sp, sunnylink_consent_version +from openpilot.common.version import terms_version, training_version, terms_version_sp, sunnylink_consent_version LayoutVariant = Literal["mici", "tizi"] diff --git a/selfdrive/ui/tests/diff/replay_script.py b/openpilot/selfdrive/ui/tests/diff/replay_script.py similarity index 99% rename from selfdrive/ui/tests/diff/replay_script.py rename to openpilot/selfdrive/ui/tests/diff/replay_script.py index ad7e8952d6..109f32e47a 100644 --- a/selfdrive/ui/tests/diff/replay_script.py +++ b/openpilot/selfdrive/ui/tests/diff/replay_script.py @@ -5,8 +5,9 @@ from dataclasses import dataclass import math -from cereal import car, log, messaging -from cereal.messaging import PubMaster +from openpilot.cereal import log, messaging +from opendbc.car.structs import car +from openpilot.cereal.messaging import PubMaster from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert @@ -121,7 +122,6 @@ def set_prime_state(prime_type: PrimeType) -> None: def setup_offroad_alerts() -> None: set_offroad_alert("Offroad_TemperatureTooHigh", True, extra_text='99C') set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text='longitudinal') - set_offroad_alert("Offroad_IsTakingSnapshot", True) def setup_update_available(available: bool = True) -> None: @@ -357,7 +357,7 @@ def build_mici_script(pm: PubMaster, main_layout, script: Script) -> None: params = Params() main_layout._alerts_layout._pending_params = ({"UpdaterNewDescription": params.get("UpdaterNewDescription")} | {alert_data.key: params.get(alert_data.key) for alert_data in main_layout._alerts_layout.sorted_alerts}) - main_layout._alerts_layout._refresh() + main_layout._alerts_layout._update_state() swipe_right(width, wait_after=WAIT_SHORT) # open alerts script.setup(setup_offroad_alerts_and_refresh) # show alerts diff --git a/selfdrive/ui/tests/profile_onroad.py b/openpilot/selfdrive/ui/tests/profile_onroad.py similarity index 95% rename from selfdrive/ui/tests/profile_onroad.py rename to openpilot/selfdrive/ui/tests/profile_onroad.py index 18194d7363..ec15e71c35 100755 --- a/selfdrive/ui/tests/profile_onroad.py +++ b/openpilot/selfdrive/ui/tests/profile_onroad.py @@ -49,7 +49,7 @@ def patch_submaster(message_chunks): sm.recv_frame[service] = sm.frame sm.valid[service] = True sm.frame += 1 - ui_state.sm.update = mock_update + ui_state.sm.update = mock_update # ty: ignore[invalid-assignment] # profiling hook if __name__ == "__main__": @@ -92,7 +92,7 @@ if __name__ == "__main__": vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, W, H) vipc.start_listener() yuv_buffer_size = W * H + (W // 2) * (H // 2) * 2 - yuv_data = np.random.randint(0, 256, yuv_buffer_size, dtype=np.uint8).tobytes() + yuv_data = np.random.default_rng().integers(0, 256, yuv_buffer_size, dtype=np.uint8).tobytes() with cProfile.Profile() as pr: for _ in gui_app.render(): if ui_state.sm.frame >= len(message_chunks): diff --git a/selfdrive/ui/tests/test_feedbackd.py b/openpilot/selfdrive/ui/tests/test_feedbackd.py similarity index 95% rename from selfdrive/ui/tests/test_feedbackd.py rename to openpilot/selfdrive/ui/tests/test_feedbackd.py index 0f7148de04..72bd499081 100644 --- a/selfdrive/ui/tests/test_feedbackd.py +++ b/openpilot/selfdrive/ui/tests/test_feedbackd.py @@ -1,6 +1,6 @@ import pytest -import cereal.messaging as messaging -from cereal import car +import openpilot.cereal.messaging as messaging +from opendbc.car.structs import car from openpilot.common.params import Params from openpilot.system.manager.process_config import managed_processes diff --git a/openpilot/selfdrive/ui/tests/test_raylib_ui.py b/openpilot/selfdrive/ui/tests/test_raylib_ui.py new file mode 100644 index 0000000000..d04c940cde --- /dev/null +++ b/openpilot/selfdrive/ui/tests/test_raylib_ui.py @@ -0,0 +1,9 @@ +import time +from openpilot.selfdrive.test.helpers import with_processes + + +class TestRaylibUi: + @with_processes(["ui"]) + def test_raylib_ui(self): + """Test initialization of the UI widgets is successful.""" + time.sleep(1) diff --git a/selfdrive/ui/tests/test_soundd.py b/openpilot/selfdrive/ui/tests/test_soundd.py similarity index 90% rename from selfdrive/ui/tests/test_soundd.py rename to openpilot/selfdrive/ui/tests/test_soundd.py index 226117ae8f..95588a9fba 100644 --- a/selfdrive/ui/tests/test_soundd.py +++ b/openpilot/selfdrive/ui/tests/test_soundd.py @@ -1,11 +1,10 @@ -from cereal import car -from cereal import messaging -from cereal.messaging import SubMaster, PubMaster +from openpilot.cereal import log, messaging +from openpilot.cereal.messaging import SubMaster, PubMaster from openpilot.selfdrive.ui.soundd import SELFDRIVE_STATE_TIMEOUT, check_selfdrive_timeout_alert import time -AudibleAlert = car.CarControl.HUDControl.AudibleAlert +AudibleAlert = log.SelfdriveState.AudibleAlert class TestSoundd: diff --git a/openpilot/selfdrive/ui/tests/test_translations.py b/openpilot/selfdrive/ui/tests/test_translations.py new file mode 100644 index 0000000000..9eae072f21 --- /dev/null +++ b/openpilot/selfdrive/ui/tests/test_translations.py @@ -0,0 +1,104 @@ +import json +import re +import string +from pathlib import Path + +import pytest + +from openpilot.selfdrive.ui.translations.potools import parse_po +from openpilot.system.ui.lib.multilang import LANGUAGES_FILE, TRANSLATIONS_DIR + +PERCENT_PLACEHOLDER_RE = re.compile(r"%(?:n|\d+)") +BAD_ENTITY_RE = re.compile(r'@(\w+);') +LINE_NUMBER_REF_RE = re.compile(r'^#:\s+.+:\d+(?:\s|$)') +FORMATTER = string.Formatter() +PO_DIR = Path(str(TRANSLATIONS_DIR)) + +with LANGUAGES_FILE.open(encoding='utf-8') as f: + TRANSLATION_LANGUAGES = json.load(f) + + +def extract_placeholders(text: str) -> list[str]: + placeholders = PERCENT_PLACEHOLDER_RE.findall(text) + + try: + parsed = list(FORMATTER.parse(text)) + except ValueError as e: + raise AssertionError(f"invalid brace formatting in {text!r}: {e}") from e + + for _, field_name, format_spec, conversion in parsed: + if field_name is None: + continue + + token = "{" + token += field_name + if conversion: + token += f"!{conversion}" + if format_spec: + token += f":{format_spec}" + token += "}" + placeholders.append(token) + + return sorted(placeholders) + + +def load_po_text(po_path: Path) -> str: + return po_path.read_text(encoding='utf-8') + + +class TestTranslations: + @pytest.mark.parametrize("language_code", sorted(TRANSLATION_LANGUAGES.values())) + def test_translation_file_exists(self, language_code: str): + po_path = PO_DIR / f"app_{language_code}.po" + assert po_path.exists(), f"missing translation file: {po_path}" + + @pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) + def test_translation_placeholders_are_preserved(self, po_path: Path): + _, entries = parse_po(po_path) + language = po_path.stem.removeprefix("app_") + + for entry in entries: + source_placeholders = extract_placeholders(entry.msgid) + + if entry.is_plural: + plural_placeholders = extract_placeholders(entry.msgid_plural) + message = ( + f"{language}: source plural placeholders do not match singular for " + + f"{entry.msgid!r}: {source_placeholders} vs {plural_placeholders}" + ) + assert plural_placeholders == source_placeholders, message + + for idx, msgstr in sorted(entry.msgstr_plural.items()): + if not msgstr: + continue + + translated_placeholders = extract_placeholders(msgstr) + message = ( + f"{language}: plural form {idx} changes placeholders for {entry.msgid!r}: " + + f"expected {source_placeholders}, got {translated_placeholders}" + ) + assert translated_placeholders == source_placeholders, message + else: + if not entry.msgstr: + continue + + translated_placeholders = extract_placeholders(entry.msgstr) + message = ( + f"{language}: translation changes placeholders for {entry.msgid!r}: " + + f"expected {source_placeholders}, got {translated_placeholders}" + ) + assert translated_placeholders == source_placeholders, message + + @pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) + def test_translation_refs_do_not_include_line_numbers(self, po_path: Path): + for line in load_po_text(po_path).splitlines(): + assert not LINE_NUMBER_REF_RE.match(line), ( + f"{po_path.name}: line-number source reference found: {line}" + ) + + @pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) + def test_translation_entities_are_valid(self, po_path: Path): + matches = BAD_ENTITY_RE.findall(load_po_text(po_path)) + assert not matches, ( + f"{po_path.name}: found '@...;' entity typo(s): {', '.join(sorted(set(matches)))}" + ) diff --git a/selfdrive/ui/translations/app.pot b/openpilot/selfdrive/ui/translations/app.pot similarity index 100% rename from selfdrive/ui/translations/app.pot rename to openpilot/selfdrive/ui/translations/app.pot diff --git a/selfdrive/ui/translations/app_de.po b/openpilot/selfdrive/ui/translations/app_de.po similarity index 100% rename from selfdrive/ui/translations/app_de.po rename to openpilot/selfdrive/ui/translations/app_de.po diff --git a/selfdrive/ui/translations/app_en.po b/openpilot/selfdrive/ui/translations/app_en.po similarity index 100% rename from selfdrive/ui/translations/app_en.po rename to openpilot/selfdrive/ui/translations/app_en.po diff --git a/selfdrive/ui/translations/app_es.po b/openpilot/selfdrive/ui/translations/app_es.po similarity index 100% rename from selfdrive/ui/translations/app_es.po rename to openpilot/selfdrive/ui/translations/app_es.po diff --git a/selfdrive/ui/translations/app_fr.po b/openpilot/selfdrive/ui/translations/app_fr.po similarity index 100% rename from selfdrive/ui/translations/app_fr.po rename to openpilot/selfdrive/ui/translations/app_fr.po diff --git a/selfdrive/ui/translations/app_ja.po b/openpilot/selfdrive/ui/translations/app_ja.po similarity index 100% rename from selfdrive/ui/translations/app_ja.po rename to openpilot/selfdrive/ui/translations/app_ja.po diff --git a/selfdrive/ui/translations/app_ko.po b/openpilot/selfdrive/ui/translations/app_ko.po similarity index 100% rename from selfdrive/ui/translations/app_ko.po rename to openpilot/selfdrive/ui/translations/app_ko.po diff --git a/selfdrive/ui/translations/app_pt-BR.po b/openpilot/selfdrive/ui/translations/app_pt-BR.po similarity index 100% rename from selfdrive/ui/translations/app_pt-BR.po rename to openpilot/selfdrive/ui/translations/app_pt-BR.po diff --git a/selfdrive/ui/translations/app_th.po b/openpilot/selfdrive/ui/translations/app_th.po similarity index 100% rename from selfdrive/ui/translations/app_th.po rename to openpilot/selfdrive/ui/translations/app_th.po diff --git a/selfdrive/ui/translations/app_tr.po b/openpilot/selfdrive/ui/translations/app_tr.po similarity index 100% rename from selfdrive/ui/translations/app_tr.po rename to openpilot/selfdrive/ui/translations/app_tr.po diff --git a/selfdrive/ui/translations/app_uk.po b/openpilot/selfdrive/ui/translations/app_uk.po similarity index 100% rename from selfdrive/ui/translations/app_uk.po rename to openpilot/selfdrive/ui/translations/app_uk.po diff --git a/selfdrive/ui/translations/app_zh-CHS.po b/openpilot/selfdrive/ui/translations/app_zh-CHS.po similarity index 100% rename from selfdrive/ui/translations/app_zh-CHS.po rename to openpilot/selfdrive/ui/translations/app_zh-CHS.po diff --git a/selfdrive/ui/translations/app_zh-CHT.po b/openpilot/selfdrive/ui/translations/app_zh-CHT.po similarity index 100% rename from selfdrive/ui/translations/app_zh-CHT.po rename to openpilot/selfdrive/ui/translations/app_zh-CHT.po diff --git a/selfdrive/ui/translations/auto_translate.sh b/openpilot/selfdrive/ui/translations/auto_translate.sh similarity index 91% rename from selfdrive/ui/translations/auto_translate.sh rename to openpilot/selfdrive/ui/translations/auto_translate.sh index 03a207ca3c..7238426c75 100755 --- a/selfdrive/ui/translations/auto_translate.sh +++ b/openpilot/selfdrive/ui/translations/auto_translate.sh @@ -15,7 +15,7 @@ command -v codex >/dev/null || { } codex exec --cd "$ROOT" -c 'model_reasoning_effort="low"' --dangerously-bypass-approvals-and-sandbox "$(cat < tuple[POEntry | None, list[POEntry]]: cur_field: str | None = None plural_idx = 0 - def finish(): - nonlocal cur, header - if cur is None: + def finish(entry: POEntry | None): + nonlocal header + if entry is None: return - if cur.msgid == "" and cur.msgstr: - header = cur - elif cur.msgid != "" or cur.is_plural: - entries.append(cur) - cur = None + if entry.msgid == "" and entry.msgstr: + header = entry + elif entry.msgid != "" or entry.is_plural: + entries.append(entry) for raw in lines: line = raw.rstrip('\n') stripped = line.strip() if not stripped: - finish() + finish(cur) + cur = None cur_field = None continue @@ -123,6 +123,8 @@ def parse_po(path: str | Path) -> tuple[POEntry | None, list[POEntry]]: continue if stripped.startswith('msgstr '): + if cur is None: + cur = POEntry() cur.msgstr = _parse_quoted(stripped[len('msgstr '):]) cur_field = 'msgstr' continue @@ -138,7 +140,7 @@ def parse_po(path: str | Path) -> tuple[POEntry | None, list[POEntry]]: elif cur_field == 'msgstr_plural': cur.msgstr_plural[plural_idx] += val - finish() + finish(cur) return header, entries diff --git a/selfdrive/ui/translations/update_translations.py b/openpilot/selfdrive/ui/translations/update_translations.py similarity index 77% rename from selfdrive/ui/translations/update_translations.py rename to openpilot/selfdrive/ui/translations/update_translations.py index 6ff3667d8a..9b0d63ee12 100755 --- a/selfdrive/ui/translations/update_translations.py +++ b/openpilot/selfdrive/ui/translations/update_translations.py @@ -12,9 +12,9 @@ POT_FILE = os.path.join(str(TRANSLATIONS_DIR), "app.pot") def update_translations(): files = [] for root, _, filenames in chain(os.walk(SYSTEM_UI_DIR), - os.walk(os.path.join(UI_DIR, "widgets")), - os.walk(os.path.join(UI_DIR, "layouts")), - os.walk(os.path.join(UI_DIR, "onroad"))): + os.walk(os.path.join(str(UI_DIR), "widgets")), + os.walk(os.path.join(str(UI_DIR), "layouts")), + os.walk(os.path.join(str(UI_DIR), "onroad"))): for filename in filenames: if filename.endswith(".py"): files.append(os.path.relpath(os.path.join(root, filename), BASEDIR)) @@ -25,7 +25,7 @@ def update_translations(): # Generate/update translation files for each language for name in multilang.languages.values(): - po_file = os.path.join(TRANSLATIONS_DIR, f"app_{name}.po") + po_file = os.path.join(str(TRANSLATIONS_DIR), f"app_{name}.po") if os.path.exists(po_file): merge_po(po_file, POT_FILE) else: diff --git a/selfdrive/ui/ui.py b/openpilot/selfdrive/ui/ui.py similarity index 93% rename from selfdrive/ui/ui.py rename to openpilot/selfdrive/ui/ui.py index e01ddb2513..ff2bc7d3ce 100755 --- a/selfdrive/ui/ui.py +++ b/openpilot/selfdrive/ui/ui.py @@ -2,8 +2,8 @@ import os import time -from cereal import messaging -from openpilot.system.hardware import TICI +from openpilot.cereal import messaging +from openpilot.common.hardware import TICI from openpilot.common.realtime import Priority, config_realtime_process, set_core_affinity from openpilot.system.ui.lib.application import gui_app from openpilot.selfdrive.ui.layouts.main import MainLayout diff --git a/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py similarity index 96% rename from selfdrive/ui/ui_state.py rename to openpilot/selfdrive/ui/ui_state.py index 806dc11a6d..59742edfed 100644 --- a/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -1,17 +1,17 @@ -import pyray as rl import numpy as np import time import threading from collections.abc import Callable from enum import Enum -from cereal import messaging, car, log +from openpilot.cereal import messaging, log +from opendbc.car.structs import car from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import drop_realtime from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.lib.prime_state import PrimeState from openpilot.system.ui.lib.application import gui_app -from openpilot.system.hardware import HARDWARE, PC +from openpilot.common.hardware import HARDWARE, PC from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP @@ -147,7 +147,7 @@ class UIState(UIStateSP): # Check ignition status across all pandas if self.panda_type != log.PandaState.PandaType.unknown: self.ignition = any(state.ignitionLine or state.ignitionCan for state in panda_states) - elif self.sm.frame - self.sm.recv_frame["pandaStates"] > 5 * rl.get_fps(): + elif not self.sm.alive["pandaStates"]: self.panda_type = log.PandaState.PandaType.unknown # Handle wide road camera state updates @@ -314,9 +314,9 @@ class Device(DeviceSP): brightness = 0 if brightness != self._last_brightness: - self._brightness_target = brightness + self._brightness_target = int(brightness) self._brightness_event.set() - self._last_brightness = brightness + self._last_brightness = int(brightness) def _update_wakefulness(self): # Handle interactive timeout @@ -337,9 +337,9 @@ class Device(DeviceSP): self._set_awake(ui_state.ignition or not interaction_timeout or PC) - def _set_awake(self, on: bool): + def _set_awake(self, on: bool, _ui_state=None): if on != self._awake: - DeviceSP._set_awake(on, ui_state) + super()._set_awake(on, _ui_state or ui_state) self._awake = on cloudlog.debug(f"setting display power {int(on)}") HARDWARE.set_display_power(on) diff --git a/selfdrive/ui/watch3.py b/openpilot/selfdrive/ui/watch3.py similarity index 100% rename from selfdrive/ui/watch3.py rename to openpilot/selfdrive/ui/watch3.py diff --git a/selfdrive/ui/tests/__init__.py b/openpilot/selfdrive/ui/widgets/__init__.py similarity index 100% rename from selfdrive/ui/tests/__init__.py rename to openpilot/selfdrive/ui/widgets/__init__.py diff --git a/selfdrive/ui/widgets/exp_mode_button.py b/openpilot/selfdrive/ui/widgets/exp_mode_button.py similarity index 100% rename from selfdrive/ui/widgets/exp_mode_button.py rename to openpilot/selfdrive/ui/widgets/exp_mode_button.py diff --git a/selfdrive/ui/widgets/offroad_alerts.py b/openpilot/selfdrive/ui/widgets/offroad_alerts.py similarity index 99% rename from selfdrive/ui/widgets/offroad_alerts.py rename to openpilot/selfdrive/ui/widgets/offroad_alerts.py index 110ca714a9..88ccc5df7e 100644 --- a/selfdrive/ui/widgets/offroad_alerts.py +++ b/openpilot/selfdrive/ui/widgets/offroad_alerts.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from collections.abc import Callable from dataclasses import dataclass from openpilot.common.params import Params -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE 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.scroll_panel import GuiScrollPanel diff --git a/selfdrive/ui/widgets/pairing_dialog.py b/openpilot/selfdrive/ui/widgets/pairing_dialog.py similarity index 100% rename from selfdrive/ui/widgets/pairing_dialog.py rename to openpilot/selfdrive/ui/widgets/pairing_dialog.py diff --git a/selfdrive/ui/widgets/prime.py b/openpilot/selfdrive/ui/widgets/prime.py similarity index 100% rename from selfdrive/ui/widgets/prime.py rename to openpilot/selfdrive/ui/widgets/prime.py diff --git a/selfdrive/ui/widgets/setup.py b/openpilot/selfdrive/ui/widgets/setup.py similarity index 100% rename from selfdrive/ui/widgets/setup.py rename to openpilot/selfdrive/ui/widgets/setup.py diff --git a/selfdrive/ui/widgets/ssh_key.py b/openpilot/selfdrive/ui/widgets/ssh_key.py similarity index 100% rename from selfdrive/ui/widgets/ssh_key.py rename to openpilot/selfdrive/ui/widgets/ssh_key.py diff --git a/openpilot/sunnypilot b/openpilot/sunnypilot deleted file mode 120000 index c4ca692907..0000000000 --- a/openpilot/sunnypilot +++ /dev/null @@ -1 +0,0 @@ -../sunnypilot \ No newline at end of file diff --git a/sunnypilot/SConscript b/openpilot/sunnypilot/SConscript similarity index 100% rename from sunnypilot/SConscript rename to openpilot/sunnypilot/SConscript diff --git a/sunnypilot/__init__.py b/openpilot/sunnypilot/__init__.py similarity index 100% rename from sunnypilot/__init__.py rename to openpilot/sunnypilot/__init__.py diff --git a/sunnypilot/common/transformations/SConscript b/openpilot/sunnypilot/common/transformations/SConscript similarity index 100% rename from sunnypilot/common/transformations/SConscript rename to openpilot/sunnypilot/common/transformations/SConscript diff --git a/sunnypilot/common/transformations/coordinates.cc b/openpilot/sunnypilot/common/transformations/coordinates.cc similarity index 100% rename from sunnypilot/common/transformations/coordinates.cc rename to openpilot/sunnypilot/common/transformations/coordinates.cc diff --git a/sunnypilot/common/transformations/coordinates.hpp b/openpilot/sunnypilot/common/transformations/coordinates.hpp similarity index 100% rename from sunnypilot/common/transformations/coordinates.hpp rename to openpilot/sunnypilot/common/transformations/coordinates.hpp diff --git a/sunnypilot/common/transformations/orientation.cc b/openpilot/sunnypilot/common/transformations/orientation.cc similarity index 100% rename from sunnypilot/common/transformations/orientation.cc rename to openpilot/sunnypilot/common/transformations/orientation.cc diff --git a/sunnypilot/common/transformations/orientation.hpp b/openpilot/sunnypilot/common/transformations/orientation.hpp similarity index 100% rename from sunnypilot/common/transformations/orientation.hpp rename to openpilot/sunnypilot/common/transformations/orientation.hpp diff --git a/openpilot/sunnypilot/common/version.h b/openpilot/sunnypilot/common/version.h new file mode 100644 index 0000000000..26151e206f --- /dev/null +++ b/openpilot/sunnypilot/common/version.h @@ -0,0 +1 @@ +#define SUNNYPILOT_VERSION "2026.003.000" diff --git a/selfdrive/ui/widgets/__init__.py b/openpilot/sunnypilot/livedelay/__init__.py similarity index 100% rename from selfdrive/ui/widgets/__init__.py rename to openpilot/sunnypilot/livedelay/__init__.py diff --git a/sunnypilot/livedelay/helpers.py b/openpilot/sunnypilot/livedelay/helpers.py similarity index 100% rename from sunnypilot/livedelay/helpers.py rename to openpilot/sunnypilot/livedelay/helpers.py diff --git a/sunnypilot/livedelay/lagd_toggle.py b/openpilot/sunnypilot/livedelay/lagd_toggle.py similarity index 97% rename from sunnypilot/livedelay/lagd_toggle.py rename to openpilot/sunnypilot/livedelay/lagd_toggle.py index 279aa353a3..3926cbebf6 100644 --- a/sunnypilot/livedelay/lagd_toggle.py +++ b/openpilot/sunnypilot/livedelay/lagd_toggle.py @@ -4,7 +4,7 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import log +from openpilot.cereal import log from opendbc.car import structs from openpilot.common.params import Params diff --git a/sunnypilot/mads/helpers.py b/openpilot/sunnypilot/mads/helpers.py similarity index 100% rename from sunnypilot/mads/helpers.py rename to openpilot/sunnypilot/mads/helpers.py diff --git a/sunnypilot/mads/mads.py b/openpilot/sunnypilot/mads/mads.py similarity index 98% rename from sunnypilot/mads/mads.py rename to openpilot/sunnypilot/mads/mads.py index bd9c8d78ff..4ae3534b57 100644 --- a/sunnypilot/mads/mads.py +++ b/openpilot/sunnypilot/mads/mads.py @@ -5,7 +5,7 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import log, custom +from openpilot.cereal import log, custom from opendbc.car import structs from opendbc.car.hyundai.values import HyundaiFlags @@ -69,7 +69,7 @@ class ModularAssistiveDrivingSystem: return False def should_silent_lkas_enable(self, CS: structs.CarState) -> bool: - if self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE and self.pedal_pressed_non_gas_pressed(CS): + if self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE and (CS.brakePressed or CS.regenBraking or self.pedal_pressed_non_gas_pressed(CS)): return False if self.events_sp.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): diff --git a/sunnypilot/mads/state.py b/openpilot/sunnypilot/mads/state.py similarity index 99% rename from sunnypilot/mads/state.py rename to openpilot/sunnypilot/mads/state.py index 73240c790c..a8e5e1892f 100644 --- a/sunnypilot/mads/state.py +++ b/openpilot/sunnypilot/mads/state.py @@ -5,7 +5,7 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import log, custom +from openpilot.cereal import log, custom from openpilot.selfdrive.selfdrived.events import ET from openpilot.selfdrive.selfdrived.state import SOFT_DISABLE_TIME from openpilot.common.realtime import DT_CTRL diff --git a/sunnypilot/mads/tests/test_mads_state_machine.py b/openpilot/sunnypilot/mads/tests/test_mads_state_machine.py similarity index 97% rename from sunnypilot/mads/tests/test_mads_state_machine.py rename to openpilot/sunnypilot/mads/tests/test_mads_state_machine.py index 7bc556a0fc..14549119fd 100644 --- a/sunnypilot/mads/tests/test_mads_state_machine.py +++ b/openpilot/sunnypilot/mads/tests/test_mads_state_machine.py @@ -8,7 +8,7 @@ See the LICENSE.md file in the root directory for more details. import pytest from pytest_mock import MockerFixture -from cereal import custom +from openpilot.cereal import custom from openpilot.common.realtime import DT_CTRL from openpilot.sunnypilot.mads.state import StateMachine, SOFT_DISABLE_TIME from openpilot.selfdrive.selfdrived.events import ET, NormalPermanentAlert, Events @@ -29,7 +29,7 @@ def make_event(event_types): event = {} for ev in event_types: event[ev] = NormalPermanentAlert("alert") - EVENTS_SP[0] = event + EVENTS_SP[0] = event # type: ignore[assignment] # ty: ignore[invalid-assignment] return 0 diff --git a/openpilot/sunnypilot/mads/tests/test_mads_steering_mode.py b/openpilot/sunnypilot/mads/tests/test_mads_steering_mode.py new file mode 100644 index 0000000000..3b7e27a86f --- /dev/null +++ b/openpilot/sunnypilot/mads/tests/test_mads_steering_mode.py @@ -0,0 +1,242 @@ +""" +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 pytest + +from openpilot.cereal import log, custom +from opendbc.car import structs +from openpilot.selfdrive.selfdrived.events import Events +from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP +from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param +from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem +from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP + +State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState +EventName = log.OnroadEvent.EventName +EventNameSP = custom.OnroadEventSP.EventName +SafetyModel = structs.CarParams.SafetyModel + + +def make_car_state(brake_pressed=False, regen_braking=False, standstill=False, v_ego=0.0): + cs = structs.CarState() + cs.brakePressed = brake_pressed + cs.regenBraking = regen_braking + cs.standstill = standstill + cs.vEgo = v_ego + cs.cruiseState.available = True + return cs + + +def make_panda_state(mocker, controls_allowed_lateral=True): + ps = mocker.MagicMock() + ps.controlsAllowedLateral = controls_allowed_lateral + ps.safetyModel = SafetyModel.hyundai + return ps + + +def make_mads(mocker, steering_mode): + sd = mocker.MagicMock() + sd.CP = structs.CarParams() + sd.CP.brand = "hyundai" + sd.CP_SP = structs.CarParamsSP() + sd.params = mocker.MagicMock() + sd.params.get_bool = mocker.MagicMock(side_effect=lambda k: { + "Mads": True, "MadsMainCruiseAllowed": False, + "DisengageOnAccelerator": True, "MadsUnifiedEngagementMode": False, + }.get(k, False)) + sd.params.get = mocker.MagicMock(return_value=steering_mode) + sd.events = Events() + sd.events_sp = EventsSP() + sd.enabled = False + sd.enabled_prev = False + sd.initialized = True + sd.CS_prev = make_car_state() + sd.sm = {'pandaStates': [make_panda_state(mocker)]} + sd.state_machine = mocker.MagicMock() + + mads = ModularAssistiveDrivingSystem(sd) + mads.enabled_toggle = True + mads.steering_mode_on_brake = steering_mode + return mads, sd + + +def run_frames(mads, sd, cs, n=1): + for _ in range(n): + mads.update(cs) + sd.CS_prev = cs + sd.events.clear() + sd.events_sp.clear() + + +# should_silent_lkas_enable across all modes + +class TestShouldSilentLkasEnable: + @pytest.mark.parametrize("brake,regen", [(True, False), (False, True)]) + def test_pause_blocks_reenable_on_braking_at_standstill(self, mocker, brake, regen): + mads, _ = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) + cs = make_car_state(brake_pressed=brake, regen_braking=regen, standstill=True) + assert mads.should_silent_lkas_enable(cs) is False + + def test_pause_allows_reenable_on_brake_release(self, mocker): + mads, _ = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) + cs = make_car_state(standstill=True) + assert mads.should_silent_lkas_enable(cs) is True + + def test_remain_active_ignores_brake(self, mocker): + mads, _ = make_mads(mocker, MadsSteeringModeOnBrake.REMAIN_ACTIVE) + cs = make_car_state(brake_pressed=True, standstill=True) + assert mads.should_silent_lkas_enable(cs) is True + + def test_disengage_ignores_brake_for_silent_enable(self, mocker): + mads, _ = make_mads(mocker, MadsSteeringModeOnBrake.DISENGAGE) + cs = make_car_state(brake_pressed=True, standstill=True) + assert mads.should_silent_lkas_enable(cs) is True + + +# pause + +class TestPauseMode: + def test_stays_paused_at_standstill_brake_held(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) + mads.state_machine.state = State.enabled + mads.enabled = True + mads.active = True + + sd.events.add(EventName.pedalPressed) + run_frames(mads, sd, make_car_state(brake_pressed=True, v_ego=15.0)) + assert mads.state_machine.state == State.paused + + sd.sm['pandaStates'] = [make_panda_state(mocker, False)] + run_frames(mads, sd, make_car_state(brake_pressed=True, standstill=True), n=250) + assert mads.state_machine.state == State.paused + + def test_resumes_on_brake_release_at_standstill(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) + mads.state_machine.state = State.paused + mads.enabled = True + mads.active = False + + run_frames(mads, sd, make_car_state(standstill=True)) + assert mads.state_machine.state == State.enabled + + def test_full_cycle_moving_to_standstill(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) + mads.state_machine.state = State.enabled + mads.enabled = True + mads.active = True + + sd.events.add(EventName.pedalPressed) + run_frames(mads, sd, make_car_state(brake_pressed=True, v_ego=15.0)) + assert mads.state_machine.state == State.paused + + sd.sm['pandaStates'] = [make_panda_state(mocker, False)] + run_frames(mads, sd, make_car_state(brake_pressed=True, standstill=True), n=250) + assert mads.state_machine.state == State.paused + + sd.sm['pandaStates'] = [make_panda_state(mocker, True)] + run_frames(mads, sd, make_car_state(standstill=True)) + assert mads.state_machine.state == State.enabled + + +# disengage + +class TestDisengageMode: + def test_brake_while_enabled_disables(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.DISENGAGE) + mads.state_machine.state = State.enabled + mads.enabled = True + mads.active = True + + sd.events.add(EventName.pedalPressed) + run_frames(mads, sd, make_car_state(brake_pressed=True, v_ego=10.0)) + assert mads.state_machine.state == State.disabled + + def test_brake_sends_lkas_disable_when_enabled(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.DISENGAGE) + mads.state_machine.state = State.enabled + mads.enabled = True + mads.active = True + + sd.events.add(EventName.pedalPressed) + mads.update_events(make_car_state(brake_pressed=True, v_ego=5.0)) + assert sd.events_sp.has(EventNameSP.lkasDisable) + + +# remain active + +class TestRemainActiveMode: + def test_brake_does_not_pause_or_disable(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.REMAIN_ACTIVE) + mads.state_machine.state = State.enabled + mads.enabled = True + mads.active = True + + sd.events.add(EventName.pedalPressed) + run_frames(mads, sd, make_car_state(brake_pressed=True, v_ego=10.0)) + assert mads.state_machine.state == State.enabled + + +# lateral mismatch counter + +class TestLateralMismatchCounter: + def test_no_accumulation_while_paused(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) + mads.state_machine.state = State.paused + mads.enabled = True + mads.active = False + sd.sm['pandaStates'] = [make_panda_state(mocker, False)] + + run_frames(mads, sd, make_car_state(brake_pressed=True, standstill=True), n=250) + assert mads.lateral_mismatch_counter == 0 + + def test_accumulates_when_active_and_panda_disagrees(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) + mads.enabled = True + mads.active = True + sd.sm['pandaStates'] = [make_panda_state(mocker, False)] + + for _ in range(200): + mads.data_sample() + assert mads.lateral_mismatch_counter == 200 + + +# brand restrictions + +class TestBrandSteeringModeRestrictions: + def test_rivian_forced_to_disengage(self, mocker): + CP = structs.CarParams() + CP.brand = "rivian" + CP_SP = structs.CarParamsSP() + params = mocker.MagicMock() + assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE + params.get.assert_not_called() + + def test_tesla_without_vehicle_bus_forced_to_disengage(self, mocker): + CP = structs.CarParams() + CP.brand = "tesla" + CP_SP = structs.CarParamsSP() + CP_SP.flags = 0 + params = mocker.MagicMock() + assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE + + def test_tesla_with_vehicle_bus_uses_param(self, mocker): + CP = structs.CarParams() + CP.brand = "tesla" + CP_SP = structs.CarParamsSP() + CP_SP.flags = TeslaFlagsSP.HAS_VEHICLE_BUS + params = mocker.MagicMock() + params.get = mocker.MagicMock(return_value=MadsSteeringModeOnBrake.REMAIN_ACTIVE) + assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.REMAIN_ACTIVE + + @pytest.mark.parametrize("brand", ["hyundai", "toyota", "honda", "gm"]) + def test_other_brands_use_param(self, mocker, brand): + CP = structs.CarParams() + CP.brand = brand + CP_SP = structs.CarParamsSP() + params = mocker.MagicMock() + params.get = mocker.MagicMock(return_value=MadsSteeringModeOnBrake.REMAIN_ACTIVE) + assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.REMAIN_ACTIVE diff --git a/sunnypilot/mapd/__init__.py b/openpilot/sunnypilot/mapd/__init__.py similarity index 56% rename from sunnypilot/mapd/__init__.py rename to openpilot/sunnypilot/mapd/__init__.py index 7ad6f74149..57525254ce 100644 --- a/sunnypilot/mapd/__init__.py +++ b/openpilot/sunnypilot/mapd/__init__.py @@ -1,5 +1,5 @@ import os from openpilot.common.basedir import BASEDIR -MAPD_BIN_DIR = os.path.join(BASEDIR, 'third_party/mapd_pfeiferj') +MAPD_BIN_DIR = os.path.join(BASEDIR, 'openpilot', 'third_party/mapd_pfeiferj') MAPD_PATH = os.path.join(MAPD_BIN_DIR, 'mapd') diff --git a/sunnypilot/mapd/live_map_data/__init__.py b/openpilot/sunnypilot/mapd/live_map_data/__init__.py similarity index 100% rename from sunnypilot/mapd/live_map_data/__init__.py rename to openpilot/sunnypilot/mapd/live_map_data/__init__.py diff --git a/sunnypilot/mapd/live_map_data/base_map_data.py b/openpilot/sunnypilot/mapd/live_map_data/base_map_data.py similarity index 97% rename from sunnypilot/mapd/live_map_data/base_map_data.py rename to openpilot/sunnypilot/mapd/live_map_data/base_map_data.py index 723864dd2a..ca79ce1c6b 100644 --- a/sunnypilot/mapd/live_map_data/base_map_data.py +++ b/openpilot/sunnypilot/mapd/live_map_data/base_map_data.py @@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details. """ from abc import abstractmethod, ABC -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.common.params import Params from openpilot.common.constants import CV from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET diff --git a/sunnypilot/mapd/live_map_data/debug.py b/openpilot/sunnypilot/mapd/live_map_data/debug.py similarity index 87% rename from sunnypilot/mapd/live_map_data/debug.py rename to openpilot/sunnypilot/mapd/live_map_data/debug.py index 794f2ef8ff..00783d58df 100644 --- a/sunnypilot/mapd/live_map_data/debug.py +++ b/openpilot/sunnypilot/mapd/live_map_data/debug.py @@ -12,12 +12,12 @@ See the LICENSE.md file in the root directory for more details. import threading import traceback -from cereal import messaging +from openpilot.cereal import messaging from openpilot.common.gps import get_gps_location_service from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_resolver import SpeedLimitResolver +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Policy +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_resolver import SpeedLimitResolver from openpilot.sunnypilot.mapd.live_map_data import get_debug diff --git a/sunnypilot/mapd/live_map_data/osm_map_data.py b/openpilot/sunnypilot/mapd/live_map_data/osm_map_data.py similarity index 98% rename from sunnypilot/mapd/live_map_data/osm_map_data.py rename to openpilot/sunnypilot/mapd/live_map_data/osm_map_data.py index e2a4ec667f..e6cbce0674 100644 --- a/sunnypilot/mapd/live_map_data/osm_map_data.py +++ b/openpilot/sunnypilot/mapd/live_map_data/osm_map_data.py @@ -8,7 +8,7 @@ import json import math import platform -from cereal import log +from openpilot.cereal import log from openpilot.common.params import Params from openpilot.sunnypilot.mapd.live_map_data.base_map_data import BaseMapData from openpilot.sunnypilot.navd.helpers import Coordinate diff --git a/sunnypilot/mapd/live_map_data/standalone.py b/openpilot/sunnypilot/mapd/live_map_data/standalone.py similarity index 100% rename from sunnypilot/mapd/live_map_data/standalone.py rename to openpilot/sunnypilot/mapd/live_map_data/standalone.py diff --git a/sunnypilot/mapd/mapd_installer.py b/openpilot/sunnypilot/mapd/mapd_installer.py similarity index 97% rename from sunnypilot/mapd/mapd_installer.py rename to openpilot/sunnypilot/mapd/mapd_installer.py index 0e287ed405..9f2e72720a 100755 --- a/sunnypilot/mapd/mapd_installer.py +++ b/openpilot/sunnypilot/mapd/mapd_installer.py @@ -14,11 +14,11 @@ import requests from pathlib import Path from urllib.request import urlopen -from cereal import messaging +from openpilot.cereal import messaging from openpilot.common.params import Params -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.spinner import Spinner -from openpilot.system.version import is_prebuilt +from openpilot.common.version import is_prebuilt from openpilot.sunnypilot.mapd import MAPD_PATH, MAPD_BIN_DIR import openpilot.system.sentry as sentry diff --git a/sunnypilot/mapd/mapd_manager.py b/openpilot/sunnypilot/mapd/mapd_manager.py similarity index 97% rename from sunnypilot/mapd/mapd_manager.py rename to openpilot/sunnypilot/mapd/mapd_manager.py index 88ee564190..2251289bbe 100755 --- a/sunnypilot/mapd/mapd_manager.py +++ b/openpilot/sunnypilot/mapd/mapd_manager.py @@ -17,7 +17,7 @@ from openpilot.common.realtime import Ratekeeper, config_realtime_process from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.sunnypilot.mapd.live_map_data.osm_map_data import OsmMapData -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.sunnypilot.mapd import MAPD_PATH from openpilot.sunnypilot.mapd.mapd_installer import VERSION, update_installed_version @@ -128,7 +128,7 @@ def main_thread(): cloudlog.exception(f"mapd: failed to make {Paths.mapd_root()}") while True: - show_alert = get_files_for_cleanup() and params.get_bool("OsmLocal") + show_alert = bool(get_files_for_cleanup() and params.get_bool("OsmLocal")) set_offroad_alert("Offroad_OSMUpdateRequired", show_alert, "This alert will be cleared when new maps are downloaded.") update_osm_db() diff --git a/sunnypilot/livedelay/__init__.py b/openpilot/sunnypilot/mapd/tests/__init__.py similarity index 100% rename from sunnypilot/livedelay/__init__.py rename to openpilot/sunnypilot/mapd/tests/__init__.py diff --git a/sunnypilot/mapd/tests/mapd_hash b/openpilot/sunnypilot/mapd/tests/mapd_hash similarity index 100% rename from sunnypilot/mapd/tests/mapd_hash rename to openpilot/sunnypilot/mapd/tests/mapd_hash diff --git a/sunnypilot/mapd/tests/test_mapd_version.py b/openpilot/sunnypilot/mapd/tests/test_mapd_version.py similarity index 100% rename from sunnypilot/mapd/tests/test_mapd_version.py rename to openpilot/sunnypilot/mapd/tests/test_mapd_version.py diff --git a/sunnypilot/mapd/update_version.py b/openpilot/sunnypilot/mapd/update_version.py similarity index 93% rename from sunnypilot/mapd/update_version.py rename to openpilot/sunnypilot/mapd/update_version.py index c5e08b3f8f..b6ca2a757f 100755 --- a/sunnypilot/mapd/update_version.py +++ b/openpilot/sunnypilot/mapd/update_version.py @@ -13,8 +13,8 @@ from openpilot.sunnypilot import get_file_hash from openpilot.common.basedir import BASEDIR from openpilot.sunnypilot.mapd import MAPD_PATH -MAPD_HASH_PATH = os.path.join(BASEDIR, "sunnypilot", "mapd", "tests", "mapd_hash") -MAPD_VERSION_PATH = os.path.join(BASEDIR, "sunnypilot", "mapd", "mapd_installer.py") +MAPD_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "mapd", "tests", "mapd_hash") +MAPD_VERSION_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "mapd", "mapd_installer.py") def update_mapd_hash(): diff --git a/sunnypilot/mapd/version.py b/openpilot/sunnypilot/mapd/version.py similarity index 100% rename from sunnypilot/mapd/version.py rename to openpilot/sunnypilot/mapd/version.py diff --git a/sunnypilot/modeld_v2/.gitignore b/openpilot/sunnypilot/modeld_v2/.gitignore similarity index 100% rename from sunnypilot/modeld_v2/.gitignore rename to openpilot/sunnypilot/modeld_v2/.gitignore diff --git a/sunnypilot/modeld_v2/SConscript b/openpilot/sunnypilot/modeld_v2/SConscript similarity index 91% rename from sunnypilot/modeld_v2/SConscript rename to openpilot/sunnypilot/modeld_v2/SConscript index a5b0bad862..81affc625a 100644 --- a/sunnypilot/modeld_v2/SConscript +++ b/openpilot/sunnypilot/modeld_v2/SConscript @@ -3,7 +3,7 @@ import glob from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE -from openpilot.system.hardware import HARDWARE, PC +from openpilot.common.hardware import HARDWARE, PC Import('env', 'arch', 'release') lenv = env.Clone() @@ -38,7 +38,7 @@ camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS) pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + ':' + env.Dir("#").abspath + '"' compile_modeld_script = File("compile_modeld.py").abspath -upstream_compile_script = File(Dir("#selfdrive/modeld").File("compile_modeld.py").abspath) +upstream_compile_script = File(Dir("#openpilot/selfdrive/modeld").File("compile_modeld.py").abspath) script_deps = [File("compile_modeld.py"), upstream_compile_script] def compile_combined(model_type, onnx_args, output_name): @@ -84,10 +84,10 @@ if os.path.isfile(supercombo_onnx): 'driving_combined_supercombo_tinygrad.pkl') if PC: - inputs = tinygrad_files + [File(Dir("#sunnypilot/modeld_v2").File("install_models_pc.py").abspath)] + inputs = tinygrad_files + [File(Dir("#openpilot/sunnypilot/modeld_v2").File("install_models_pc.py").abspath)] outputs = [] model_dir = Dir("models").abspath - cmd = f'python3 {Dir("#sunnypilot/modeld_v2").abspath}/install_models_pc.py {model_dir}' + cmd = f'python3 {Dir("#openpilot/sunnypilot/modeld_v2").abspath}/install_models_pc.py {model_dir}' for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_on_policy', 'driving_policy']: if File(f"models/{model_name}.onnx").exists(): diff --git a/sunnypilot/modeld_v2/__init__.py b/openpilot/sunnypilot/modeld_v2/__init__.py similarity index 100% rename from sunnypilot/modeld_v2/__init__.py rename to openpilot/sunnypilot/modeld_v2/__init__.py diff --git a/sunnypilot/modeld_v2/camera_offset_helper.py b/openpilot/sunnypilot/modeld_v2/camera_offset_helper.py similarity index 100% rename from sunnypilot/modeld_v2/camera_offset_helper.py rename to openpilot/sunnypilot/modeld_v2/camera_offset_helper.py diff --git a/sunnypilot/modeld_v2/compile_modeld.py b/openpilot/sunnypilot/modeld_v2/compile_modeld.py similarity index 100% rename from sunnypilot/modeld_v2/compile_modeld.py rename to openpilot/sunnypilot/modeld_v2/compile_modeld.py diff --git a/sunnypilot/modeld_v2/constants.py b/openpilot/sunnypilot/modeld_v2/constants.py similarity index 100% rename from sunnypilot/modeld_v2/constants.py rename to openpilot/sunnypilot/modeld_v2/constants.py diff --git a/sunnypilot/modeld_v2/fill_model_msg.py b/openpilot/sunnypilot/modeld_v2/fill_model_msg.py similarity index 99% rename from sunnypilot/modeld_v2/fill_model_msg.py rename to openpilot/sunnypilot/modeld_v2/fill_model_msg.py index 1f281258ba..7d7854be11 100644 --- a/sunnypilot/modeld_v2/fill_model_msg.py +++ b/openpilot/sunnypilot/modeld_v2/fill_model_msg.py @@ -1,7 +1,7 @@ import os import capnp import numpy as np -from cereal import log +from openpilot.cereal import log from openpilot.sunnypilot.modeld_v2.constants import ModelConstants, Plan from openpilot.sunnypilot.models.helpers import plan_x_idxs_helper from openpilot.selfdrive.controls.lib.drive_helpers import get_curvature_from_plan diff --git a/sunnypilot/modeld_v2/get_model_metadata.py b/openpilot/sunnypilot/modeld_v2/get_model_metadata.py similarity index 100% rename from sunnypilot/modeld_v2/get_model_metadata.py rename to openpilot/sunnypilot/modeld_v2/get_model_metadata.py diff --git a/sunnypilot/modeld_v2/install_models_pc.py b/openpilot/sunnypilot/modeld_v2/install_models_pc.py similarity index 92% rename from sunnypilot/modeld_v2/install_models_pc.py rename to openpilot/sunnypilot/modeld_v2/install_models_pc.py index 1bba001abd..7bc2f4797c 100755 --- a/sunnypilot/modeld_v2/install_models_pc.py +++ b/openpilot/sunnypilot/modeld_v2/install_models_pc.py @@ -5,8 +5,8 @@ import pickle import codecs from pathlib import Path -from openpilot.system.hardware.hw import Paths -from sunnypilot.modeld_v2.get_model_metadata import MetadataOnnxPBParser, get_name_and_shape, get_metadata_value_by_name +from openpilot.common.hardware.hw import Paths +from openpilot.sunnypilot.modeld_v2.get_model_metadata import MetadataOnnxPBParser, get_name_and_shape, get_metadata_value_by_name def generate_metadata_pkl(model_path, output_path): diff --git a/sunnypilot/modeld_v2/meta_20hz.py b/openpilot/sunnypilot/modeld_v2/meta_20hz.py similarity index 100% rename from sunnypilot/modeld_v2/meta_20hz.py rename to openpilot/sunnypilot/modeld_v2/meta_20hz.py diff --git a/sunnypilot/modeld_v2/meta_helper.py b/openpilot/sunnypilot/modeld_v2/meta_helper.py similarity index 96% rename from sunnypilot/modeld_v2/meta_helper.py rename to openpilot/sunnypilot/modeld_v2/meta_helper.py index 3fa10b2415..bcf73adc6f 100644 --- a/sunnypilot/modeld_v2/meta_helper.py +++ b/openpilot/sunnypilot/modeld_v2/meta_helper.py @@ -1,5 +1,5 @@ from openpilot.sunnypilot.modeld_v2.constants import Meta -from cereal import custom +from openpilot.cereal import custom from openpilot.sunnypilot.modeld_v2.meta_20hz import Meta20hz from openpilot.sunnypilot.models.helpers import get_active_bundle diff --git a/sunnypilot/modeld_v2/modeld b/openpilot/sunnypilot/modeld_v2/modeld similarity index 100% rename from sunnypilot/modeld_v2/modeld rename to openpilot/sunnypilot/modeld_v2/modeld diff --git a/sunnypilot/modeld_v2/modeld.py b/openpilot/sunnypilot/modeld_v2/modeld.py similarity index 100% rename from sunnypilot/modeld_v2/modeld.py rename to openpilot/sunnypilot/modeld_v2/modeld.py diff --git a/sunnypilot/modeld_v2/modeld_base.py b/openpilot/sunnypilot/modeld_v2/modeld_base.py similarity index 100% rename from sunnypilot/modeld_v2/modeld_base.py rename to openpilot/sunnypilot/modeld_v2/modeld_base.py diff --git a/sunnypilot/modeld_v2/parse_model_outputs.py b/openpilot/sunnypilot/modeld_v2/parse_model_outputs.py similarity index 97% rename from sunnypilot/modeld_v2/parse_model_outputs.py rename to openpilot/sunnypilot/modeld_v2/parse_model_outputs.py index c71a146454..82103283f3 100644 --- a/sunnypilot/modeld_v2/parse_model_outputs.py +++ b/openpilot/sunnypilot/modeld_v2/parse_model_outputs.py @@ -61,6 +61,7 @@ class Parser: weights[fidx] = weights[fidx][idxs] pred_mu[fidx] = pred_mu[fidx][idxs] pred_std[fidx] = pred_std[fidx][idxs] + assert out_shape is not None full_shape = tuple([raw.shape[0], in_N] + list(out_shape)) outs[name + '_weights'] = weights outs[name + '_hypotheses'] = pred_mu.reshape(full_shape) @@ -78,8 +79,10 @@ class Parser: pred_std_final = pred_std if out_N > 1: + assert out_shape is not None final_shape = tuple([raw.shape[0], out_N] + list(out_shape)) else: + assert out_shape is not None final_shape = tuple([raw.shape[0],] + list(out_shape)) outs[name] = pred_mu_final.reshape(final_shape) outs[name + '_stds'] = pred_std_final.reshape(final_shape) diff --git a/sunnypilot/modeld_v2/parse_model_outputs_split.py b/openpilot/sunnypilot/modeld_v2/parse_model_outputs_split.py similarity index 98% rename from sunnypilot/modeld_v2/parse_model_outputs_split.py rename to openpilot/sunnypilot/modeld_v2/parse_model_outputs_split.py index 7848e3e185..efc5d846da 100644 --- a/sunnypilot/modeld_v2/parse_model_outputs_split.py +++ b/openpilot/sunnypilot/modeld_v2/parse_model_outputs_split.py @@ -65,6 +65,7 @@ class Parser: weights[fidx] = weights[fidx][idxs] pred_mu[fidx] = pred_mu[fidx][idxs] pred_std[fidx] = pred_std[fidx][idxs] + assert out_shape is not None full_shape = tuple([raw.shape[0], in_N] + list(out_shape)) outs[name + '_weights'] = weights outs[name + '_hypotheses'] = pred_mu.reshape(full_shape) @@ -82,8 +83,10 @@ class Parser: pred_std_final = pred_std if out_N > 1: + assert out_shape is not None final_shape = tuple([raw.shape[0], out_N] + list(out_shape)) else: + assert out_shape is not None final_shape = tuple([raw.shape[0],] + list(out_shape)) outs[name] = pred_mu_final.reshape(final_shape) outs[name + '_stds'] = pred_std_final.reshape(final_shape) diff --git a/sunnypilot/mapd/tests/__init__.py b/openpilot/sunnypilot/modeld_v2/tests/__init__.py similarity index 100% rename from sunnypilot/mapd/tests/__init__.py rename to openpilot/sunnypilot/modeld_v2/tests/__init__.py diff --git a/sunnypilot/modeld_v2/tests/conftest.py b/openpilot/sunnypilot/modeld_v2/tests/conftest.py similarity index 99% rename from sunnypilot/modeld_v2/tests/conftest.py rename to openpilot/sunnypilot/modeld_v2/tests/conftest.py index 89e2f2d4cc..f79cbe10b2 100644 --- a/sunnypilot/modeld_v2/tests/conftest.py +++ b/openpilot/sunnypilot/modeld_v2/tests/conftest.py @@ -192,7 +192,7 @@ def patch_modeld(monkeypatch): @pytest.fixture def model_state_factory(tmp_path, monkeypatch, patch_modeld): - from openpilot.system.hardware import hw + from openpilot.common.hardware import hw def _create(archetype): write_pkl(tmp_path, archetype) diff --git a/sunnypilot/modeld_v2/tests/test_camera_offset_helper.py b/openpilot/sunnypilot/modeld_v2/tests/test_camera_offset_helper.py similarity index 100% rename from sunnypilot/modeld_v2/tests/test_camera_offset_helper.py rename to openpilot/sunnypilot/modeld_v2/tests/test_camera_offset_helper.py diff --git a/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py b/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py similarity index 91% rename from sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py rename to openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py index 38f6cd446b..b13a7abecf 100644 --- a/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py +++ b/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py @@ -35,7 +35,7 @@ class TestFindDrivingPkl: def test_finds_pkl_by_artifact_name(self, tmp_path, monkeypatch): (tmp_path / 'driving_fof_tinygrad.pkl').write_bytes(b'fake') - from openpilot.system.hardware import hw + from openpilot.common.hardware import hw monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path))) bundle = DummyBundle(models=[ @@ -67,13 +67,20 @@ class TestStockEquivalence: state = model_state_factory(ARCHETYPES['vision_policy_split']) frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES) - stock_queues, stock_npy = make_input_queues(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES, frame_skip, - device='NPY') + # action_t is a deep-model prerequisite the SP loader doesn't provide yet; see skip_keys below + stock_shapes = {**SPLIT_VISION_INPUT_SHAPES, **SPLIT_POLICY_INPUT_SHAPES, 'action_t': (1, 2)} + stock_queues, stock_npy = make_input_queues(stock_shapes, frame_skip, device='NPY') # TODO-SP: remove action_t skip once SP adds prerequisite for deep models (action_t input queue) - skip_keys = {'action_t'} - assert set(state.input_queues.keys()) == set(stock_queues.keys()) - skip_keys, \ - f"Queue keys differ: v2={set(state.input_queues.keys())}, stock={set(stock_queues.keys())}" + # prev_feat is a stock QCOM corruption workaround handled inside the SP loader's JIT path + skip_keys = {'action_t', 'prev_feat'} + # stock packs the per-key policy inputs into packed_npy_inputs; the npy views carry the individual keys + stock_queue_keys = set(stock_queues.keys()) + if 'packed_npy_inputs' in stock_queue_keys: + stock_queue_keys.remove('packed_npy_inputs') + stock_queue_keys |= set(stock_npy.keys()) + assert set(state.input_queues.keys()) == stock_queue_keys - skip_keys, \ + f"Queue keys differ: v2={set(state.input_queues.keys())}, stock={stock_queue_keys}" assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - skip_keys, \ f"Npy keys differ: v2={set(state.numpy_inputs.keys())}, stock={set(stock_npy.keys())}" @@ -223,7 +230,7 @@ class TestMlsimProperty: assert state.mlsim is False def test_mlsim_true_for_gen11(self, tmp_path, monkeypatch, patch_modeld): - from openpilot.system.hardware import hw + from openpilot.common.hardware import hw from openpilot.sunnypilot.modeld_v2.tests.conftest import write_pkl, ARCHETYPES as A arch = A['supercombo_non20hz'] @@ -238,7 +245,7 @@ class TestMlsimProperty: class TestCrossArchetypeMismatch: def test_wrong_is_20hz_changes_constants(self, tmp_path, monkeypatch, patch_modeld): - from openpilot.system.hardware import hw + from openpilot.common.hardware import hw from openpilot.sunnypilot.modeld_v2.tests.conftest import write_pkl from openpilot.sunnypilot.modeld_v2.constants import ModelConstants diff --git a/sunnypilot/modeld_v2/tests/test_compile_modeld.py b/openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py similarity index 97% rename from sunnypilot/modeld_v2/tests/test_compile_modeld.py rename to openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py index f404678516..c30dd46e33 100644 --- a/sunnypilot/modeld_v2/tests/test_compile_modeld.py +++ b/openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py @@ -61,7 +61,7 @@ class TestFrameSkipBufferLengthEquivalence: class TestTemporalSamplingEquivalence: def test_non20hz_desire_sampling_identity(self): - buf = np.random.randn(100, 1, 8).astype(np.float32) + buf = np.random.default_rng(0).standard_normal((100, 1, 8)).astype(np.float32) frame_skip = 1 sampled = buf[::frame_skip].reshape(-1, 8) assert sampled.shape == (100, 8) @@ -150,7 +150,7 @@ class TestOutputSlicePreservation: def test_vision_hidden_state_slice_used_for_features(self): mock_slices = {'hidden_state': slice(0, 512), 'plan': slice(512, 1024)} features_slice = mock_slices['hidden_state'] - fake_output = np.random.randn(1, 1024).astype(np.float32) + fake_output = np.random.default_rng(0).standard_normal((1, 1024)).astype(np.float32) features = fake_output[:, features_slice] assert features.shape == (1, 512) diff --git a/sunnypilot/modeld_v2/tests/test_recovery_power.py b/openpilot/sunnypilot/modeld_v2/tests/test_recovery_power.py similarity index 81% rename from sunnypilot/modeld_v2/tests/test_recovery_power.py rename to openpilot/sunnypilot/modeld_v2/tests/test_recovery_power.py index cfa2272386..d7c8563e64 100644 --- a/sunnypilot/modeld_v2/tests/test_recovery_power.py +++ b/openpilot/sunnypilot/modeld_v2/tests/test_recovery_power.py @@ -1,6 +1,8 @@ +from typing import Any + import numpy as np -from cereal import log +from openpilot.cereal import log from openpilot.sunnypilot.modeld_v2.constants import Plan from openpilot.sunnypilot.modeld_v2.modeld import ModelState @@ -14,7 +16,7 @@ class MockStruct: def test_recovery_power_scaling(): - state = MockStruct( + state: Any = MockStruct( PLANPLUS_CONTROL=0.75, LONG_SMOOTH_SECONDS=0.3, LAT_SMOOTH_SECONDS=0.1, @@ -35,10 +37,10 @@ def test_recovery_power_scaling(): recorded_curv_plans.append(plan.copy()) return 0.0 - modeld.get_accel_from_plan = mock_accel - modeld.get_curvature_from_output = mock_curvature - plan = np.random.rand(1, 100, 15).astype(np.float32) - planplus = np.random.rand(1, 100, 15).astype(np.float32) + modeld.get_accel_from_plan = mock_accel # ty: ignore[invalid-assignment] + modeld.get_curvature_from_output = mock_curvature # ty: ignore[invalid-assignment] + plan = np.random.default_rng(0).random((1, 100, 15)).astype(np.float32) + planplus = np.random.default_rng(1).random((1, 100, 15)).astype(np.float32) merged_plan = plan + planplus model_output: dict = { @@ -60,7 +62,7 @@ def test_recovery_power_scaling(): state.PLANPLUS_CONTROL = control recorded_vel.clear() recorded_curv_plans.clear() - ModelState.get_action_from_model(state, model_output, prev_action, 0.0, 0.0, v_ego) + ModelState.get_action_from_model(state, model_output, prev_action, 0.0, 0.0, v_ego) # type: ignore[arg-type] expected_accel_plan_vel = plan[0, :, Plan.VELOCITY][:, 0] + planplus[0, :, Plan.VELOCITY][:, 0] np.testing.assert_allclose(recorded_vel[0], expected_accel_plan_vel, rtol=1e-5, atol=1e-6) diff --git a/sunnypilot/models/README.md b/openpilot/sunnypilot/models/README.md similarity index 100% rename from sunnypilot/models/README.md rename to openpilot/sunnypilot/models/README.md diff --git a/sunnypilot/modeld_v2/tests/__init__.py b/openpilot/sunnypilot/models/__init__.py similarity index 100% rename from sunnypilot/modeld_v2/tests/__init__.py rename to openpilot/sunnypilot/models/__init__.py diff --git a/sunnypilot/models/constants.py b/openpilot/sunnypilot/models/constants.py similarity index 100% rename from sunnypilot/models/constants.py rename to openpilot/sunnypilot/models/constants.py diff --git a/sunnypilot/models/default_model.py b/openpilot/sunnypilot/models/default_model.py similarity index 75% rename from sunnypilot/models/default_model.py rename to openpilot/sunnypilot/models/default_model.py index 1d1064863f..62b6831402 100755 --- a/sunnypilot/models/default_model.py +++ b/openpilot/sunnypilot/models/default_model.py @@ -6,17 +6,15 @@ from openpilot.common.basedir import BASEDIR from openpilot.sunnypilot import get_file_hash from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL -DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "sunnypilot", "models", "model_name.py") -MODEL_HASH_PATH = os.path.join(BASEDIR, "sunnypilot", "models", "tests", "model_hash") -VISION_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_vision.onnx") -POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_on_policy.onnx") +DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py") +MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash") +SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "driving_supercombo.onnx") def update_model_hash(): - vision_hash = get_file_hash(VISION_ONNX_PATH) - policy_hash = get_file_hash(POLICY_ONNX_PATH) + supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH) - combined_hash = hashlib.sha256((vision_hash + policy_hash).encode()).hexdigest() + combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest() with open(MODEL_HASH_PATH, "w") as f: f.write(combined_hash) diff --git a/sunnypilot/models/fetcher.py b/openpilot/sunnypilot/models/fetcher.py similarity index 99% rename from sunnypilot/models/fetcher.py rename to openpilot/sunnypilot/models/fetcher.py index e0829c8909..8b545e14a6 100644 --- a/sunnypilot/models/fetcher.py +++ b/openpilot/sunnypilot/models/fetcher.py @@ -14,7 +14,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.system.hardware.hw import Paths from openpilot.sunnypilot.models.helpers import is_bundle_version_compatible -from cereal import custom +from openpilot.cereal import custom class ModelParser: diff --git a/sunnypilot/models/helpers.py b/openpilot/sunnypilot/models/helpers.py similarity index 95% rename from sunnypilot/models/helpers.py rename to openpilot/sunnypilot/models/helpers.py index 0fa94e39ff..ad1333d4ed 100644 --- a/sunnypilot/models/helpers.py +++ b/openpilot/sunnypilot/models/helpers.py @@ -11,11 +11,11 @@ import pickle from pathlib import Path import numpy as np -from cereal import custom +from openpilot.cereal import custom from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRaider -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths # SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO REQUIRED_JSON_VERSION = 15 @@ -27,9 +27,10 @@ _LAST_VALIDATED_RAW = None def _compute_hash(file_path: str) -> str | None: - from openpilot.common.file_chunker import read_file_chunked + from openpilot.common.file_chunker import open_file_chunked try: - return hashlib.sha256(read_file_chunked(file_path)).hexdigest().lower() + with open_file_chunked(file_path) as file: + return hashlib.file_digest(file, "sha256").hexdigest().lower() except FileNotFoundError: return None @@ -125,7 +126,7 @@ def get_active_bundle(params: Params | None = None, raw_bundle_dict: dict | byte params = params or Params() try: active_bundle_dict = raw_bundle_dict if raw_bundle_dict is not None else (params.get("ModelManager_ActiveBundle") or {}) - if active_bundle_dict and is_bundle_version_compatible(active_bundle_dict): + if isinstance(active_bundle_dict, dict) and active_bundle_dict and is_bundle_version_compatible(active_bundle_dict): return custom.ModelManagerSP.ModelBundle(**active_bundle_dict) except Exception: pass diff --git a/sunnypilot/models/manager.py b/openpilot/sunnypilot/models/manager.py similarity index 96% rename from sunnypilot/models/manager.py rename to openpilot/sunnypilot/models/manager.py index f4db071c7b..6b04dcd04c 100644 --- a/sunnypilot/models/manager.py +++ b/openpilot/sunnypilot/models/manager.py @@ -13,9 +13,9 @@ import aiohttp from openpilot.common.params import Params from openpilot.common.realtime import Ratekeeper from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths -from cereal import messaging, custom +from openpilot.cereal import messaging, custom from openpilot.sunnypilot.models.fetcher import ModelFetcher from openpilot.sunnypilot.models.helpers import get_active_bundle, validate_active_bundle, verify_file @@ -69,7 +69,7 @@ class ModelManagerSP: total_size = int(response.headers.get("content-length", 0)) bytes_downloaded = 0 - with open(path, 'wb') as f: + with open(path, 'wb') as f: # noqa: ASYNC230 async for chunk in response.content.iter_chunked(self._chunk_size): # type: bytes f.write(chunk) bytes_downloaded += len(chunk) @@ -106,7 +106,7 @@ class ModelManagerSP: async with session.get(chunk_url) as response: response.raise_for_status() chunk_size = int(response.headers.get("content-length", 0)) - with open(chunk_path, 'wb') as f: + with open(chunk_path, 'wb') as f: # noqa: ASYNC230 async for data in response.content.iter_chunked(self._chunk_size): f.write(data) chunk_downloaded += len(data) @@ -120,9 +120,9 @@ class ModelManagerSP: self._sync_artifact_progress(artifact) self._report_status() - with open(manifest_path, 'w') as f: + with open(manifest_path, 'w') as f: # noqa: ASYNC230 f.write(str(num_chunks)) - if os.path.isfile(base_path): + if os.path.isfile(base_path): # noqa: ASYNC240 os.remove(base_path) del self._download_start_times[artifact.fileName] @@ -161,7 +161,7 @@ class ModelManagerSP: except Exception as e: cloudlog.error(f"Error downloading {filename}: {str(e)}") for f in [full_path] + [p for p in (os.path.join(destination_path, f) for f in os.listdir(destination_path)) if filename in p]: - if os.path.isfile(f): + if os.path.isfile(f): # noqa: ASYNC240 os.remove(f) artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed artifact.downloadProgress.eta = 0 @@ -215,7 +215,7 @@ class ModelManagerSP: self.selected_bundle = None except Exception: - if self.selected_bundle: + if self.selected_bundle is not None: self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed raise diff --git a/sunnypilot/models/model_name.py b/openpilot/sunnypilot/models/model_name.py similarity index 100% rename from sunnypilot/models/model_name.py rename to openpilot/sunnypilot/models/model_name.py diff --git a/sunnypilot/models/runners/constants.py b/openpilot/sunnypilot/models/runners/constants.py similarity index 79% rename from sunnypilot/models/runners/constants.py rename to openpilot/sunnypilot/models/runners/constants.py index acb316888c..344f8f98cc 100644 --- a/sunnypilot/models/runners/constants.py +++ b/openpilot/sunnypilot/models/runners/constants.py @@ -1,7 +1,7 @@ import os import numpy as np -from openpilot.system.hardware.hw import Paths -from cereal import custom +from openpilot.common.hardware.hw import Paths +from openpilot.cereal import custom # Type definitions for clarity NumpyDict = dict[str, np.ndarray] diff --git a/sunnypilot/models/split_model_constants.py b/openpilot/sunnypilot/models/split_model_constants.py similarity index 100% rename from sunnypilot/models/split_model_constants.py rename to openpilot/sunnypilot/models/split_model_constants.py diff --git a/sunnypilot/models/__init__.py b/openpilot/sunnypilot/models/tests/__init__.py similarity index 100% rename from sunnypilot/models/__init__.py rename to openpilot/sunnypilot/models/tests/__init__.py diff --git a/openpilot/sunnypilot/models/tests/model_hash b/openpilot/sunnypilot/models/tests/model_hash new file mode 100644 index 0000000000..13a16294ff --- /dev/null +++ b/openpilot/sunnypilot/models/tests/model_hash @@ -0,0 +1 @@ +49133798d9cd9cacf47085c7ef8122bfee88cd9c6192a8314c81bfb1b37f5809 \ No newline at end of file diff --git a/sunnypilot/models/tests/model_manager_audit.py b/openpilot/sunnypilot/models/tests/model_manager_audit.py similarity index 93% rename from sunnypilot/models/tests/model_manager_audit.py rename to openpilot/sunnypilot/models/tests/model_manager_audit.py index 4cd2b7d78e..2fc8bf5f21 100644 --- a/sunnypilot/models/tests/model_manager_audit.py +++ b/openpilot/sunnypilot/models/tests/model_manager_audit.py @@ -5,7 +5,7 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import messaging, custom +from openpilot.cereal import messaging, custom if __name__ == "__main__": sm = messaging.SubMaster(["modelManagerSP"]) diff --git a/sunnypilot/models/tests/test_default_model.py b/openpilot/sunnypilot/models/tests/test_default_model.py similarity index 73% rename from sunnypilot/models/tests/test_default_model.py rename to openpilot/sunnypilot/models/tests/test_default_model.py index 7c2fde70a8..ab51027442 100644 --- a/sunnypilot/models/tests/test_default_model.py +++ b/openpilot/sunnypilot/models/tests/test_default_model.py @@ -6,16 +6,15 @@ See the LICENSE.md file in the root directory for more details. """ from openpilot.sunnypilot import get_file_hash -from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, VISION_ONNX_PATH, POLICY_ONNX_PATH +from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, SUPERCOMBO_ONNX_PATH import hashlib class TestDefaultModel: def test_compare_onnx_hashes(self): - vision_hash = get_file_hash(VISION_ONNX_PATH) - policy_hash = get_file_hash(POLICY_ONNX_PATH) + supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH) - combined_hash = hashlib.sha256((vision_hash + policy_hash).encode()).hexdigest() + combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest() with open(MODEL_HASH_PATH) as f: current_hash = f.read().strip() diff --git a/sunnypilot/models/tests/test_tinygrad_ref.py b/openpilot/sunnypilot/models/tests/test_tinygrad_ref.py similarity index 100% rename from sunnypilot/models/tests/test_tinygrad_ref.py rename to openpilot/sunnypilot/models/tests/test_tinygrad_ref.py diff --git a/sunnypilot/models/tinygrad_ref.py b/openpilot/sunnypilot/models/tinygrad_ref.py similarity index 100% rename from sunnypilot/models/tinygrad_ref.py rename to openpilot/sunnypilot/models/tinygrad_ref.py diff --git a/sunnypilot/navd/helpers.py b/openpilot/sunnypilot/navd/helpers.py similarity index 100% rename from sunnypilot/navd/helpers.py rename to openpilot/sunnypilot/navd/helpers.py diff --git a/sunnypilot/neural_network_data b/openpilot/sunnypilot/neural_network_data similarity index 100% rename from sunnypilot/neural_network_data rename to openpilot/sunnypilot/neural_network_data diff --git a/sunnypilot/models/tests/__init__.py b/openpilot/sunnypilot/selfdrive/__init__.py similarity index 100% rename from sunnypilot/models/tests/__init__.py rename to openpilot/sunnypilot/selfdrive/__init__.py diff --git a/sunnypilot/selfdrive/assets/icons/clock.png b/openpilot/sunnypilot/selfdrive/assets/icons/clock.png similarity index 100% rename from sunnypilot/selfdrive/assets/icons/clock.png rename to openpilot/sunnypilot/selfdrive/assets/icons/clock.png diff --git a/sunnypilot/selfdrive/assets/icons/star-empty.png b/openpilot/sunnypilot/selfdrive/assets/icons/star-empty.png similarity index 100% rename from sunnypilot/selfdrive/assets/icons/star-empty.png rename to openpilot/sunnypilot/selfdrive/assets/icons/star-empty.png diff --git a/sunnypilot/selfdrive/assets/icons/star-filled.png b/openpilot/sunnypilot/selfdrive/assets/icons/star-filled.png similarity index 100% rename from sunnypilot/selfdrive/assets/icons/star-filled.png rename to openpilot/sunnypilot/selfdrive/assets/icons/star-filled.png diff --git a/sunnypilot/selfdrive/assets/icons_mici/always_offroad.png b/openpilot/sunnypilot/selfdrive/assets/icons_mici/always_offroad.png similarity index 100% rename from sunnypilot/selfdrive/assets/icons_mici/always_offroad.png rename to openpilot/sunnypilot/selfdrive/assets/icons_mici/always_offroad.png diff --git a/sunnypilot/selfdrive/assets/icons_mici/disable_offroad.png b/openpilot/sunnypilot/selfdrive/assets/icons_mici/disable_offroad.png similarity index 100% rename from sunnypilot/selfdrive/assets/icons_mici/disable_offroad.png rename to openpilot/sunnypilot/selfdrive/assets/icons_mici/disable_offroad.png diff --git a/sunnypilot/selfdrive/assets/images/green_light.png b/openpilot/sunnypilot/selfdrive/assets/images/green_light.png similarity index 100% rename from sunnypilot/selfdrive/assets/images/green_light.png rename to openpilot/sunnypilot/selfdrive/assets/images/green_light.png diff --git a/sunnypilot/selfdrive/assets/images/lead_depart.png b/openpilot/sunnypilot/selfdrive/assets/images/lead_depart.png similarity index 100% rename from sunnypilot/selfdrive/assets/images/lead_depart.png rename to openpilot/sunnypilot/selfdrive/assets/images/lead_depart.png diff --git a/sunnypilot/selfdrive/assets/images/spinner_sunnypilot.png b/openpilot/sunnypilot/selfdrive/assets/images/spinner_sunnypilot.png similarity index 100% rename from sunnypilot/selfdrive/assets/images/spinner_sunnypilot.png rename to openpilot/sunnypilot/selfdrive/assets/images/spinner_sunnypilot.png diff --git a/sunnypilot/selfdrive/assets/img_minus_arrow_down.png b/openpilot/sunnypilot/selfdrive/assets/img_minus_arrow_down.png similarity index 100% rename from sunnypilot/selfdrive/assets/img_minus_arrow_down.png rename to openpilot/sunnypilot/selfdrive/assets/img_minus_arrow_down.png diff --git a/sunnypilot/selfdrive/assets/img_plus_arrow_up.png b/openpilot/sunnypilot/selfdrive/assets/img_plus_arrow_up.png similarity index 100% rename from sunnypilot/selfdrive/assets/img_plus_arrow_up.png rename to openpilot/sunnypilot/selfdrive/assets/img_plus_arrow_up.png diff --git a/sunnypilot/selfdrive/assets/logo.png b/openpilot/sunnypilot/selfdrive/assets/logo.png similarity index 100% rename from sunnypilot/selfdrive/assets/logo.png rename to openpilot/sunnypilot/selfdrive/assets/logo.png diff --git a/sunnypilot/selfdrive/assets/offroad/icon_display.png b/openpilot/sunnypilot/selfdrive/assets/offroad/icon_display.png similarity index 100% rename from sunnypilot/selfdrive/assets/offroad/icon_display.png rename to openpilot/sunnypilot/selfdrive/assets/offroad/icon_display.png diff --git a/sunnypilot/selfdrive/assets/offroad/icon_exit_offroad.png b/openpilot/sunnypilot/selfdrive/assets/offroad/icon_exit_offroad.png similarity index 100% rename from sunnypilot/selfdrive/assets/offroad/icon_exit_offroad.png rename to openpilot/sunnypilot/selfdrive/assets/offroad/icon_exit_offroad.png diff --git a/sunnypilot/selfdrive/assets/offroad/icon_firehose.png b/openpilot/sunnypilot/selfdrive/assets/offroad/icon_firehose.png similarity index 100% rename from sunnypilot/selfdrive/assets/offroad/icon_firehose.png rename to openpilot/sunnypilot/selfdrive/assets/offroad/icon_firehose.png diff --git a/sunnypilot/selfdrive/assets/offroad/icon_firehose.svg b/openpilot/sunnypilot/selfdrive/assets/offroad/icon_firehose.svg similarity index 100% rename from sunnypilot/selfdrive/assets/offroad/icon_firehose.svg rename to openpilot/sunnypilot/selfdrive/assets/offroad/icon_firehose.svg diff --git a/sunnypilot/selfdrive/assets/offroad/icon_home.png b/openpilot/sunnypilot/selfdrive/assets/offroad/icon_home.png similarity index 100% rename from sunnypilot/selfdrive/assets/offroad/icon_home.png rename to openpilot/sunnypilot/selfdrive/assets/offroad/icon_home.png diff --git a/sunnypilot/selfdrive/assets/offroad/icon_home.svg b/openpilot/sunnypilot/selfdrive/assets/offroad/icon_home.svg similarity index 100% rename from sunnypilot/selfdrive/assets/offroad/icon_home.svg rename to openpilot/sunnypilot/selfdrive/assets/offroad/icon_home.svg diff --git a/sunnypilot/selfdrive/assets/offroad/icon_lateral.png b/openpilot/sunnypilot/selfdrive/assets/offroad/icon_lateral.png similarity index 100% rename from sunnypilot/selfdrive/assets/offroad/icon_lateral.png rename to openpilot/sunnypilot/selfdrive/assets/offroad/icon_lateral.png diff --git a/sunnypilot/selfdrive/assets/offroad/icon_map.png b/openpilot/sunnypilot/selfdrive/assets/offroad/icon_map.png similarity index 100% rename from sunnypilot/selfdrive/assets/offroad/icon_map.png rename to openpilot/sunnypilot/selfdrive/assets/offroad/icon_map.png diff --git a/sunnypilot/selfdrive/assets/offroad/icon_models.png b/openpilot/sunnypilot/selfdrive/assets/offroad/icon_models.png similarity index 100% rename from sunnypilot/selfdrive/assets/offroad/icon_models.png rename to openpilot/sunnypilot/selfdrive/assets/offroad/icon_models.png diff --git a/sunnypilot/selfdrive/assets/offroad/icon_software.png b/openpilot/sunnypilot/selfdrive/assets/offroad/icon_software.png similarity index 100% rename from sunnypilot/selfdrive/assets/offroad/icon_software.png rename to openpilot/sunnypilot/selfdrive/assets/offroad/icon_software.png diff --git a/sunnypilot/selfdrive/assets/offroad/icon_toggle.png b/openpilot/sunnypilot/selfdrive/assets/offroad/icon_toggle.png similarity index 100% rename from sunnypilot/selfdrive/assets/offroad/icon_toggle.png rename to openpilot/sunnypilot/selfdrive/assets/offroad/icon_toggle.png diff --git a/sunnypilot/selfdrive/assets/offroad/icon_trips.png b/openpilot/sunnypilot/selfdrive/assets/offroad/icon_trips.png similarity index 100% rename from sunnypilot/selfdrive/assets/offroad/icon_trips.png rename to openpilot/sunnypilot/selfdrive/assets/offroad/icon_trips.png diff --git a/sunnypilot/selfdrive/assets/offroad/icon_vehicle.png b/openpilot/sunnypilot/selfdrive/assets/offroad/icon_vehicle.png similarity index 100% rename from sunnypilot/selfdrive/assets/offroad/icon_vehicle.png rename to openpilot/sunnypilot/selfdrive/assets/offroad/icon_vehicle.png diff --git a/sunnypilot/selfdrive/assets/offroad/icon_visuals.png b/openpilot/sunnypilot/selfdrive/assets/offroad/icon_visuals.png similarity index 100% rename from sunnypilot/selfdrive/assets/offroad/icon_visuals.png rename to openpilot/sunnypilot/selfdrive/assets/offroad/icon_visuals.png diff --git a/sunnypilot/selfdrive/__init__.py b/openpilot/sunnypilot/selfdrive/car/__init__.py similarity index 100% rename from sunnypilot/selfdrive/__init__.py rename to openpilot/sunnypilot/selfdrive/car/__init__.py diff --git a/openpilot/sunnypilot/selfdrive/car/car_list.json b/openpilot/sunnypilot/selfdrive/car/car_list.json new file mode 120000 index 0000000000..9bef560d9c --- /dev/null +++ b/openpilot/sunnypilot/selfdrive/car/car_list.json @@ -0,0 +1 @@ +../../../../opendbc_repo/opendbc/sunnypilot/car/car_list.json \ No newline at end of file diff --git a/sunnypilot/selfdrive/car/car_specific.py b/openpilot/sunnypilot/selfdrive/car/car_specific.py similarity index 97% rename from sunnypilot/selfdrive/car/car_specific.py rename to openpilot/sunnypilot/selfdrive/car/car_specific.py index bce496ed28..54fcd34976 100644 --- a/sunnypilot/selfdrive/car/car_specific.py +++ b/openpilot/sunnypilot/selfdrive/car/car_specific.py @@ -5,7 +5,7 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import log, custom +from openpilot.cereal import log, custom from opendbc.car import structs from opendbc.car.chrysler.values import RAM_DT diff --git a/sunnypilot/selfdrive/car/cruise_ext.py b/openpilot/sunnypilot/selfdrive/car/cruise_ext.py similarity index 98% rename from sunnypilot/selfdrive/car/cruise_ext.py rename to openpilot/sunnypilot/selfdrive/car/cruise_ext.py index 3691c35972..61bec477f2 100644 --- a/sunnypilot/selfdrive/car/cruise_ext.py +++ b/openpilot/sunnypilot/selfdrive/car/cruise_ext.py @@ -6,7 +6,8 @@ See the LICENSE.md file in the root directory for more details. """ import numpy as np -from cereal import car, custom +from openpilot.cereal import custom +from opendbc.car.structs import car from opendbc.car import structs from openpilot.common.constants import CV from openpilot.common.params import Params diff --git a/sunnypilot/selfdrive/car/cruise_helpers.py b/openpilot/sunnypilot/selfdrive/car/cruise_helpers.py similarity index 96% rename from sunnypilot/selfdrive/car/cruise_helpers.py rename to openpilot/sunnypilot/selfdrive/car/cruise_helpers.py index 263d6f58cd..aa689ec904 100644 --- a/sunnypilot/selfdrive/car/cruise_helpers.py +++ b/openpilot/sunnypilot/selfdrive/car/cruise_helpers.py @@ -5,7 +5,8 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import car, custom +from openpilot.cereal import custom +from opendbc.car.structs import car from opendbc.car import structs from openpilot.common.params import Params diff --git a/sunnypilot/selfdrive/car/__init__.py b/openpilot/sunnypilot/selfdrive/car/intelligent_cruise_button_management/__init__.py similarity index 100% rename from sunnypilot/selfdrive/car/__init__.py rename to openpilot/sunnypilot/selfdrive/car/intelligent_cruise_button_management/__init__.py diff --git a/sunnypilot/selfdrive/car/intelligent_cruise_button_management/controller.py b/openpilot/sunnypilot/selfdrive/car/intelligent_cruise_button_management/controller.py similarity index 98% rename from sunnypilot/selfdrive/car/intelligent_cruise_button_management/controller.py rename to openpilot/sunnypilot/selfdrive/car/intelligent_cruise_button_management/controller.py index 1f491e0f58..ded2c0ab19 100644 --- a/sunnypilot/selfdrive/car/intelligent_cruise_button_management/controller.py +++ b/openpilot/sunnypilot/selfdrive/car/intelligent_cruise_button_management/controller.py @@ -4,7 +4,8 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import car, custom +from openpilot.cereal import custom +from opendbc.car.structs import car from opendbc.car import structs, apply_hysteresis from openpilot.common.constants import CV from openpilot.common.realtime import DT_CTRL diff --git a/sunnypilot/selfdrive/car/intelligent_cruise_button_management/helpers.py b/openpilot/sunnypilot/selfdrive/car/intelligent_cruise_button_management/helpers.py similarity index 100% rename from sunnypilot/selfdrive/car/intelligent_cruise_button_management/helpers.py rename to openpilot/sunnypilot/selfdrive/car/intelligent_cruise_button_management/helpers.py diff --git a/sunnypilot/selfdrive/car/interfaces.py b/openpilot/sunnypilot/selfdrive/car/interfaces.py similarity index 100% rename from sunnypilot/selfdrive/car/interfaces.py rename to openpilot/sunnypilot/selfdrive/car/interfaces.py diff --git a/sunnypilot/selfdrive/car/sync_sunnylink_params.py b/openpilot/sunnypilot/selfdrive/car/sync_sunnylink_params.py similarity index 88% rename from sunnypilot/selfdrive/car/sync_sunnylink_params.py rename to openpilot/sunnypilot/selfdrive/car/sync_sunnylink_params.py index 7539e741c6..6c964710b0 100755 --- a/sunnypilot/selfdrive/car/sync_sunnylink_params.py +++ b/openpilot/sunnypilot/selfdrive/car/sync_sunnylink_params.py @@ -12,7 +12,7 @@ from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog -CAR_LIST_JSON_OUT = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "car", "car_list.json") +CAR_LIST_JSON_OUT = os.path.join(BASEDIR, "openpilot", "sunnypilot", "selfdrive", "car", "car_list.json") def update_car_list_param(): diff --git a/sunnypilot/selfdrive/car/intelligent_cruise_button_management/__init__.py b/openpilot/sunnypilot/selfdrive/car/tests/__init__.py similarity index 100% rename from sunnypilot/selfdrive/car/intelligent_cruise_button_management/__init__.py rename to openpilot/sunnypilot/selfdrive/car/tests/__init__.py diff --git a/sunnypilot/selfdrive/car/tests/test_cruise_mode.py b/openpilot/sunnypilot/selfdrive/car/tests/test_cruise_mode.py similarity index 98% rename from sunnypilot/selfdrive/car/tests/test_cruise_mode.py rename to openpilot/sunnypilot/selfdrive/car/tests/test_cruise_mode.py index 2fad14a703..3f3701b3ae 100644 --- a/sunnypilot/selfdrive/car/tests/test_cruise_mode.py +++ b/openpilot/sunnypilot/selfdrive/car/tests/test_cruise_mode.py @@ -1,4 +1,4 @@ -from cereal import car +from opendbc.car.structs import car from openpilot.common.parameterized import parameterized_class from openpilot.selfdrive.selfdrived.events import Events from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper, DISTANCE_LONG_PRESS diff --git a/sunnypilot/selfdrive/car/tests/test_custom_cruise.py b/openpilot/sunnypilot/selfdrive/car/tests/test_custom_cruise.py similarity index 99% rename from sunnypilot/selfdrive/car/tests/test_custom_cruise.py rename to openpilot/sunnypilot/selfdrive/car/tests/test_custom_cruise.py index 878bc78b03..ce5d7dc124 100644 --- a/sunnypilot/selfdrive/car/tests/test_custom_cruise.py +++ b/openpilot/sunnypilot/selfdrive/car/tests/test_custom_cruise.py @@ -1,6 +1,6 @@ import pytest -from cereal import car +from opendbc.car.structs import car from openpilot.common.constants import CV from openpilot.common.parameterized import parameterized_class from openpilot.common.params import Params diff --git a/sunnypilot/selfdrive/car/tests/__init__.py b/openpilot/sunnypilot/selfdrive/controls/__init__.py similarity index 100% rename from sunnypilot/selfdrive/car/tests/__init__.py rename to openpilot/sunnypilot/selfdrive/controls/__init__.py diff --git a/sunnypilot/selfdrive/controls/controlsd_ext.py b/openpilot/sunnypilot/selfdrive/controls/controlsd_ext.py similarity index 94% rename from sunnypilot/selfdrive/controls/controlsd_ext.py rename to openpilot/sunnypilot/selfdrive/controls/controlsd_ext.py index c8d054243f..33cc9e3ad8 100644 --- a/sunnypilot/selfdrive/controls/controlsd_ext.py +++ b/openpilot/sunnypilot/selfdrive/controls/controlsd_ext.py @@ -6,8 +6,8 @@ See the LICENSE.md file in the root directory for more details. """ import time -import cereal.messaging as messaging -from cereal import log, custom +import openpilot.cereal.messaging as messaging +from openpilot.cereal import log, custom from opendbc.car import structs from openpilot.common.params import Params @@ -72,14 +72,14 @@ class ControlsExt(ModelStateBase): _lead.dRel = src.dRel _lead.yRel = src.yRel _lead.vRel = src.vRel - _lead.aRel = src.aRel + _lead.aRel = src.deprecated.aRel _lead.vLead = src.vLead - _lead.dPath = src.dPath - _lead.vLat = src.vLat + _lead.dPath = src.deprecated.dPath + _lead.vLat = src.deprecated.vLat _lead.vLeadK = src.vLeadK _lead.aLeadK = src.aLeadK - _lead.fcw = src.fcw - _lead.status = src.status + _lead.fcw = src.deprecated.fcw + _lead.status = src.present _lead.aLeadTau = src.aLeadTau _lead.modelProb = src.modelProb _lead.radar = src.radar diff --git a/sunnypilot/selfdrive/controls/__init__.py b/openpilot/sunnypilot/selfdrive/controls/lib/__init__.py similarity index 100% rename from sunnypilot/selfdrive/controls/__init__.py rename to openpilot/sunnypilot/selfdrive/controls/lib/__init__.py diff --git a/sunnypilot/selfdrive/controls/lib/auto_lane_change.py b/openpilot/sunnypilot/selfdrive/controls/lib/auto_lane_change.py similarity index 99% rename from sunnypilot/selfdrive/controls/lib/auto_lane_change.py rename to openpilot/sunnypilot/selfdrive/controls/lib/auto_lane_change.py index cf8de2a1b2..39909ac7b1 100644 --- a/sunnypilot/selfdrive/controls/lib/auto_lane_change.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/auto_lane_change.py @@ -4,7 +4,7 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import log +from openpilot.cereal import log from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL diff --git a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py b/openpilot/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py similarity index 97% rename from sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py rename to openpilot/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py index 98757cd532..e9a6be595e 100644 --- a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py @@ -4,7 +4,7 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import car +from opendbc.car.structs import car from openpilot.common.constants import CV from openpilot.common.params import Params diff --git a/sunnypilot/selfdrive/controls/lib/__init__.py b/openpilot/sunnypilot/selfdrive/controls/lib/dec/__init__.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/__init__.py rename to openpilot/sunnypilot/selfdrive/controls/lib/dec/__init__.py diff --git a/sunnypilot/selfdrive/controls/lib/dec/constants.py b/openpilot/sunnypilot/selfdrive/controls/lib/dec/constants.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/dec/constants.py rename to openpilot/sunnypilot/selfdrive/controls/lib/dec/constants.py diff --git a/sunnypilot/selfdrive/controls/lib/dec/dec.py b/openpilot/sunnypilot/selfdrive/controls/lib/dec/dec.py similarity index 99% rename from sunnypilot/selfdrive/controls/lib/dec/dec.py rename to openpilot/sunnypilot/selfdrive/controls/lib/dec/dec.py index 46cad1b048..1ba5ab0618 100644 --- a/sunnypilot/selfdrive/controls/lib/dec/dec.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/dec/dec.py @@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details. """ # Version = 2025-6-30 -from cereal import messaging +from openpilot.cereal import messaging from opendbc.car import structs from numpy import interp from openpilot.common.params import Params @@ -217,7 +217,7 @@ class DynamicExperimentalController: self._standstill_count = max(0, self._standstill_count - 1) # Lead detection - self._lead_filter.add_data(float(lead_one.status)) + self._lead_filter.add_data(float(lead_one.present)) lead_value = self._lead_filter.get_value() or 0.0 self._has_lead_filtered = lead_value > WMACConstants.LEAD_PROB diff --git a/sunnypilot/selfdrive/controls/lib/dec/__init__.py b/openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/__init__.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/dec/__init__.py rename to openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/__init__.py diff --git a/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py b/openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py similarity index 96% rename from sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py rename to openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py index f9da39c03b..407ed0af3a 100644 --- a/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py @@ -84,7 +84,7 @@ def test_radarless_slowdown_triggers_blended(mock_cp, mock_mpc, default_sm): controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) # Force conditions to simulate slowdown - controller._slow_down_filter = FakeKalman(value=1.0) # Ensure urgency triggers slowdown + controller._slow_down_filter = FakeKalman(value=1.0) # ty: ignore[invalid-assignment] controller._v_ego_kph = 35.0 default_sm['modelV2'] = MockModelData(valid=False) # Incomplete trajectory diff --git a/sunnypilot/selfdrive/controls/lib/e2e_alerts_helper.py b/openpilot/sunnypilot/selfdrive/controls/lib/e2e_alerts_helper.py similarity index 98% rename from sunnypilot/selfdrive/controls/lib/e2e_alerts_helper.py rename to openpilot/sunnypilot/selfdrive/controls/lib/e2e_alerts_helper.py index 944bf617e9..b75bae2461 100644 --- a/sunnypilot/selfdrive/controls/lib/e2e_alerts_helper.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/e2e_alerts_helper.py @@ -5,7 +5,7 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import messaging, custom +from openpilot.cereal import messaging, custom from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL @@ -61,7 +61,7 @@ class E2EAlertsHelper: model_x = sm['modelV2'].position.x max_idx = len(model_x) - 1 - self.has_lead = sm['radarState'].leadOne.status + self.has_lead = sm['radarState'].leadOne.present lead_dRel = sm['radarState'].leadOne.dRel standstill = CS.standstill diff --git a/sunnypilot/selfdrive/controls/lib/lane_turn_desire.py b/openpilot/sunnypilot/selfdrive/controls/lib/lane_turn_desire.py similarity index 97% rename from sunnypilot/selfdrive/controls/lib/lane_turn_desire.py rename to openpilot/sunnypilot/selfdrive/controls/lib/lane_turn_desire.py index fa35ebb125..ad7e80eda0 100644 --- a/sunnypilot/selfdrive/controls/lib/lane_turn_desire.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/lane_turn_desire.py @@ -4,7 +4,7 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import custom +from openpilot.cereal import custom from openpilot.common.constants import CV from openpilot.common.params import Params diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py rename to openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py rename to openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_override.py b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_override.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_override.py rename to openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_override.py diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py similarity index 99% rename from sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py rename to openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py index f7874cc539..4d9e4492f9 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py @@ -2,7 +2,7 @@ import math import numpy as np from collections import deque -from cereal import log +from openpilot.cereal import log from opendbc.car.lateral import get_friction from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.common.filter_simple import FirstOrderFilter diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_versions.json b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_versions.json similarity index 100% rename from sunnypilot/selfdrive/controls/lib/latcontrol_torque_versions.json rename to openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_versions.json diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/openpilot/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py similarity index 99% rename from sunnypilot/selfdrive/controls/lib/longitudinal_planner.py rename to openpilot/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 6efda4585f..f1e0c36416 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -5,7 +5,7 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import messaging, custom +from openpilot.cereal import messaging, custom from opendbc.car import structs from openpilot.common.constants import CV from openpilot.selfdrive.car.cruise import V_CRUISE_MAX diff --git a/sunnypilot/selfdrive/controls/lib/dec/tests/__init__.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/__init__.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/dec/tests/__init__.py rename to openpilot/sunnypilot/selfdrive/controls/lib/nnlc/__init__.py diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py similarity index 92% rename from sunnypilot/selfdrive/controls/lib/nnlc/helpers.py rename to openpilot/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py index c34fa435c2..a5d56b1ff0 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/helpers.py @@ -11,8 +11,8 @@ from difflib import SequenceMatcher from opendbc.car import structs from openpilot.common.basedir import BASEDIR -TORQUE_NN_MODEL_PATH = os.path.join(BASEDIR, "sunnypilot", "neural_network_data", "neural_network_lateral_control") -TORQUE_NN_MODEL_SUBSTITUTE_PATH = os.path.join(BASEDIR, "opendbc", "car", "torque_data/substitute.toml") +TORQUE_NN_MODEL_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "neural_network_data", "neural_network_lateral_control") +TORQUE_NN_MODEL_SUBSTITUTE_PATH = os.path.join(BASEDIR, "opendbc_repo", "opendbc", "car", "torque_data/substitute.toml") MOCK_MODEL_PATH = os.path.join(TORQUE_NN_MODEL_PATH, "MOCK.json") diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/model.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/model.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/nnlc/model.py rename to openpilot/sunnypilot/selfdrive/controls/lib/nnlc/model.py diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py similarity index 98% rename from sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py rename to openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py index 1738a11e49..e66072f86f 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -78,7 +78,7 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): self.torque_params, gravity_adjusted=False) torque_from_measurement = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._measurement, self._roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=False) - self._pid_log.error = float(torque_from_setpoint - torque_from_measurement) + self._pid_log.error = float(torque_from_setpoint - torque_from_measurement) # ty: ignore[invalid-assignment] self._ff = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._gravity_adjusted_lateral_accel, self._roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=True) self._ff += get_friction_in_torque_space(self._desired_lateral_accel - self._actual_lateral_accel, self._lateral_accel_deadzone, @@ -132,7 +132,7 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): + past_rolls + future_rolls torque_from_setpoint = self.model.evaluate(nnff_setpoint_input) torque_from_measurement = self.model.evaluate(nnff_measurement_input) - self._pid_log.error = torque_from_setpoint - torque_from_measurement + self._pid_log.error = torque_from_setpoint - torque_from_measurement # ty: ignore[invalid-assignment] # The "pure" NNLC error response can be too weak for cars whose models were trained # with a lack of high-magnitude lateral acceleration data, for which the NNLC model @@ -148,7 +148,7 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): nnff_error_input = [CS.vEgo, self._setpoint - self._measurement, self.lateral_jerk_setpoint - self.lateral_jerk_measurement, 0.0] torque_from_error = self.model.evaluate(nnff_error_input) if sign(self._pid_log.error) == sign(torque_from_error) and abs(self._pid_log.error) < abs(torque_from_error): - self._pid_log.error = self._pid_log.error * (1.0 - error_blend_factor) + torque_from_error * error_blend_factor + self._pid_log.error = self._pid_log.error * (1.0 - error_blend_factor) + torque_from_error * error_blend_factor # ty: ignore[invalid-assignment] # compute feedforward (same as nn setpoint output) friction_input = self.update_friction_input(self._setpoint, self._measurement) diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/__init__.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/__init__.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/nnlc/__init__.py rename to openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/__init__.py diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py rename to openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py rename to openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py similarity index 98% rename from sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py rename to openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py index f2009926cf..56fd2f9ce7 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py @@ -1,6 +1,7 @@ import numpy as np -from cereal import car, log, messaging +from openpilot.cereal import log, messaging +from opendbc.car.structs import car from opendbc.car.car_helpers import interfaces from opendbc.car.gm.values import CAR as GM from opendbc.car.honda.values import CAR as HONDA diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/__init__.py b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/__init__.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/smart_cruise_control/__init__.py rename to openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/__init__.py diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/map_controller.py b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/map_controller.py similarity index 98% rename from sunnypilot/selfdrive/controls/lib/smart_cruise_control/map_controller.py rename to openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/map_controller.py index c7f11a1bb2..f3ed0fc07b 100644 --- a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/map_controller.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/map_controller.py @@ -2,7 +2,7 @@ import json import math import platform -from cereal import custom +from openpilot.cereal import custom from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET @@ -151,8 +151,8 @@ class SmartCruiseControlMap: a = 0.5 * TARGET_JERK b = self.a_ego c = self.v_ego - tv - t_a = -1 * ((b**2 - 4 * a * c) ** 0.5 + b) / 2 * a - t_b = ((b**2 - 4 * a * c) ** 0.5 - b) / 2 * a + t_a = -1 * ((b**2 - 4 * a * c) ** 0.5 + b) / (2 * a) + t_b = ((b**2 - 4 * a * c) ** 0.5 - b) / (2 * a) if not isinstance(t_a, complex) and t_a > 0: t = t_a else: diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/smart_cruise_control.py b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/smart_cruise_control.py similarity index 94% rename from sunnypilot/selfdrive/controls/lib/smart_cruise_control/smart_cruise_control.py rename to openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/smart_cruise_control.py index 4ca45202fc..ff6c706634 100644 --- a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/smart_cruise_control.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/smart_cruise_control.py @@ -4,7 +4,7 @@ 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 cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.map_controller import SmartCruiseControlMap diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_map_controller.py b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_map_controller.py similarity index 70% rename from sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_map_controller.py rename to openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_map_controller.py index 254e85f573..dc27447c0b 100644 --- a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_map_controller.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_map_controller.py @@ -4,13 +4,17 @@ 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 json +import math import platform -from cereal import custom +import pytest + +from openpilot.cereal import custom from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET -from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.map_controller import SmartCruiseControlMap +from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.map_controller import R, SmartCruiseControlMap MapState = VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.MapState @@ -55,4 +59,17 @@ class TestSmartCruiseControlMap: self.scc_m.update(True, False, 0., 0., 0.) assert self.scc_m.state == VisionState.enabled + def test_moderate_curve(self): + # Regression: `... / 2 * a` parsed as `(.../2)*a` instead of `.../(2*a)`, + # making max_d ~11x too small so the moderate-curve branch never tripped. + # v_ego=25, a_ego=0, tv=24: fixed max_d≈45m vs buggy ≈4m at a 40m waypoint. + waypoint_lon_deg = (40.0 / R) * (180.0 / math.pi) + self.mem_params.put("LastGPSPosition", json.dumps({"latitude": 0.0, "longitude": 0.0}), block=True) + self.mem_params.put("MapTargetVelocities", + json.dumps([{"latitude": 0.0, "longitude": waypoint_lon_deg, "velocity": 24.0}]), block=True) + + self.scc_m.update(True, False, 25.0, 0.0, 30.0) + + assert self.scc_m.v_target == pytest.approx(24.0) + # TODO-SP: mock data from modelV2 to test other states diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py similarity index 97% rename from sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py rename to openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py index 2a2c6a6be1..98b3ffc8e7 100644 --- a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py @@ -4,11 +4,13 @@ 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 typing import Any + import numpy as np import pytest -import cereal.messaging as messaging -from cereal import custom, log +import openpilot.cereal.messaging as messaging +from openpilot.cereal import custom, log from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET @@ -113,7 +115,7 @@ class TestSmartCruiseControlVision: mdl = generate_modelV2() cs = generate_carState() controls_state = generate_controlsState() - self.sm = {'modelV2': mdl.modelV2, 'carState': cs.carState, 'controlsState': controls_state.controlsState} + self.sm: Any = {'modelV2': mdl.modelV2, 'carState': cs.carState, 'controlsState': controls_state.controlsState} def reset_params(self): self.params.put_bool("SmartCruiseControlVision", True, block=True) diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py similarity index 98% rename from sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py rename to openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py index a9d2a66227..fb76f5b546 100644 --- a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py @@ -6,8 +6,8 @@ See the LICENSE.md file in the root directory for more details. """ import numpy as np -import cereal.messaging as messaging -from cereal import custom +import openpilot.cereal.messaging as messaging +from openpilot.cereal import custom from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit/__init__.py b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/__init__.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/speed_limit/__init__.py rename to openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/__init__.py diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit/common.py b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/common.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/speed_limit/common.py rename to openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/common.py diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit/helpers.py b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/helpers.py similarity index 95% rename from sunnypilot/selfdrive/controls/lib/speed_limit/helpers.py rename to openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/helpers.py index 9eddceb820..77d89251e2 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit/helpers.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/helpers.py @@ -5,7 +5,8 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import custom, car +from openpilot.cereal import custom +from opendbc.car.structs import car from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_assist.py b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_assist.py similarity index 99% rename from sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_assist.py rename to openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_assist.py index ff7be8a8be..1ff541167f 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_assist.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_assist.py @@ -6,7 +6,8 @@ See the LICENSE.md file in the root directory for more details. """ import time -from cereal import custom, car +from openpilot.cereal import custom +from opendbc.car.structs import car from openpilot.common.params import Params from openpilot.common.constants import CV from openpilot.common.realtime import DT_MDL diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_resolver.py b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_resolver.py similarity index 97% rename from sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_resolver.py rename to openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_resolver.py index 35965c0e18..a226e0d120 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_resolver.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_resolver.py @@ -6,8 +6,8 @@ See the LICENSE.md file in the root directory for more details. """ import time -import cereal.messaging as messaging -from cereal import custom +import openpilot.cereal.messaging as messaging +from openpilot.cereal import custom from openpilot.common.constants import CV from openpilot.common.gps import get_gps_location_service from openpilot.common.params import Params @@ -152,9 +152,9 @@ class SpeedLimitResolver: self.distance_solutions[SpeedLimitSource.map] = distance_to_speed_limit_ahead def _get_source_solution_according_to_policy(self) -> custom.LongitudinalPlanSP.SpeedLimit.Source: - sources_for_policy = self._policy_to_sources_map[self.policy] + sources_for_policy = self._policy_to_sources_map[Policy(self.policy)] - if self.policy != Policy.combined: + if Policy(self.policy) != Policy.combined: # They are ordered in the order of preference, so we pick the first that's non-zero for source in sources_for_policy: if self.limit_solutions[source] > 0.: diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/__init__.py b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/__init__.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/nnlc/tests/__init__.py rename to openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/__init__.py diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_assist.py b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_assist.py similarity index 99% rename from sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_assist.py rename to openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_assist.py index 6dbe32eaf4..7cd6fef524 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_assist.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_assist.py @@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details. import pytest -from cereal import custom +from openpilot.cereal import custom from opendbc.car.car_helpers import interfaces from opendbc.car.rivian.values import CAR as RIVIAN from opendbc.car.tesla.values import CAR as TESLA diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_resolver.py b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_resolver.py similarity index 99% rename from sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_resolver.py rename to openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_resolver.py index c02831d71a..a18880c620 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_resolver.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_resolver.py @@ -10,7 +10,7 @@ import time import pytest from pytest_mock import MockerFixture -from cereal import custom +from openpilot.cereal import custom from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import LIMIT_MAX_MAP_DATA_AGE from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_resolver import SpeedLimitResolver, ALL_SOURCES diff --git a/sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py rename to openpilot/sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py diff --git a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py similarity index 99% rename from sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py rename to openpilot/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py index b547ea96b6..7a72cfa1f2 100644 --- a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py @@ -4,7 +4,7 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import car +from opendbc.car.structs import car from openpilot.common.constants import CV from openpilot.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral diff --git a/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py similarity index 99% rename from sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py rename to openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py index 57fe7b684f..c3e96fd778 100644 --- a/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py @@ -1,5 +1,5 @@ import pytest -from cereal import log, custom +from openpilot.cereal import log, custom from openpilot.common.params import Params from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper diff --git a/sunnypilot/selfdrive/locationd/.gitignore b/openpilot/sunnypilot/selfdrive/locationd/.gitignore similarity index 100% rename from sunnypilot/selfdrive/locationd/.gitignore rename to openpilot/sunnypilot/selfdrive/locationd/.gitignore diff --git a/sunnypilot/selfdrive/locationd/SConscript b/openpilot/sunnypilot/selfdrive/locationd/SConscript similarity index 100% rename from sunnypilot/selfdrive/locationd/SConscript rename to openpilot/sunnypilot/selfdrive/locationd/SConscript diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit/tests/__init__.py b/openpilot/sunnypilot/selfdrive/locationd/__init__.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/speed_limit/tests/__init__.py rename to openpilot/sunnypilot/selfdrive/locationd/__init__.py diff --git a/sunnypilot/selfdrive/locationd/locationd.cc b/openpilot/sunnypilot/selfdrive/locationd/locationd.cc similarity index 100% rename from sunnypilot/selfdrive/locationd/locationd.cc rename to openpilot/sunnypilot/selfdrive/locationd/locationd.cc diff --git a/sunnypilot/selfdrive/locationd/locationd.h b/openpilot/sunnypilot/selfdrive/locationd/locationd.h similarity index 100% rename from sunnypilot/selfdrive/locationd/locationd.h rename to openpilot/sunnypilot/selfdrive/locationd/locationd.h diff --git a/sunnypilot/selfdrive/locationd/models/.gitignore b/openpilot/sunnypilot/selfdrive/locationd/models/.gitignore similarity index 100% rename from sunnypilot/selfdrive/locationd/models/.gitignore rename to openpilot/sunnypilot/selfdrive/locationd/models/.gitignore diff --git a/sunnypilot/selfdrive/locationd/__init__.py b/openpilot/sunnypilot/selfdrive/locationd/models/__init__.py similarity index 100% rename from sunnypilot/selfdrive/locationd/__init__.py rename to openpilot/sunnypilot/selfdrive/locationd/models/__init__.py diff --git a/sunnypilot/selfdrive/locationd/models/car_kf.py b/openpilot/sunnypilot/selfdrive/locationd/models/car_kf.py similarity index 100% rename from sunnypilot/selfdrive/locationd/models/car_kf.py rename to openpilot/sunnypilot/selfdrive/locationd/models/car_kf.py diff --git a/sunnypilot/selfdrive/locationd/models/constants.py b/openpilot/sunnypilot/selfdrive/locationd/models/constants.py similarity index 100% rename from sunnypilot/selfdrive/locationd/models/constants.py rename to openpilot/sunnypilot/selfdrive/locationd/models/constants.py diff --git a/sunnypilot/selfdrive/locationd/models/live_kf.cc b/openpilot/sunnypilot/selfdrive/locationd/models/live_kf.cc similarity index 100% rename from sunnypilot/selfdrive/locationd/models/live_kf.cc rename to openpilot/sunnypilot/selfdrive/locationd/models/live_kf.cc diff --git a/sunnypilot/selfdrive/locationd/models/live_kf.h b/openpilot/sunnypilot/selfdrive/locationd/models/live_kf.h similarity index 100% rename from sunnypilot/selfdrive/locationd/models/live_kf.h rename to openpilot/sunnypilot/selfdrive/locationd/models/live_kf.h diff --git a/sunnypilot/selfdrive/locationd/models/live_kf.py b/openpilot/sunnypilot/selfdrive/locationd/models/live_kf.py similarity index 100% rename from sunnypilot/selfdrive/locationd/models/live_kf.py rename to openpilot/sunnypilot/selfdrive/locationd/models/live_kf.py diff --git a/sunnypilot/selfdrive/locationd/tests/.gitignore b/openpilot/sunnypilot/selfdrive/locationd/tests/.gitignore similarity index 100% rename from sunnypilot/selfdrive/locationd/tests/.gitignore rename to openpilot/sunnypilot/selfdrive/locationd/tests/.gitignore diff --git a/sunnypilot/selfdrive/locationd/models/__init__.py b/openpilot/sunnypilot/selfdrive/locationd/tests/__init__.py similarity index 100% rename from sunnypilot/selfdrive/locationd/models/__init__.py rename to openpilot/sunnypilot/selfdrive/locationd/tests/__init__.py diff --git a/sunnypilot/selfdrive/locationd/tests/test_locationd.py b/openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py similarity index 94% rename from sunnypilot/selfdrive/locationd/tests/test_locationd.py rename to openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py index 1f5bfd3b25..f81d1349aa 100644 --- a/sunnypilot/selfdrive/locationd/tests/test_locationd.py +++ b/openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py @@ -5,8 +5,8 @@ import random import time import capnp -import cereal.messaging as messaging -from cereal.services import SERVICE_LIST +import openpilot.cereal.messaging as messaging +from openpilot.cereal.services import SERVICE_LIST from openpilot.common.params import Params from openpilot.common.transformations.coordinates import ecef2geodetic @@ -26,7 +26,6 @@ class TestLocationdProc: self.params = Params() self.params.put_bool("UbloxAvailable", True) - managed_processes['locationd_llk'].prepare() managed_processes['locationd_llk'].start() def teardown_method(self): @@ -85,7 +84,7 @@ class TestLocationdProc: for msg in sorted(msgs, key=lambda x: x.logMonoTime): self.pm.send(msg.which(), msg) if msg.which() == "cameraOdometry": - self.pm.wait_for_readers_to_update(msg.which(), 0.1, dt=0.005) + self.pm.wait_for_readers_to_update(msg.which(), timeout=1, dt=0.005) time.sleep(1) # wait for async params write lastGPS = json.loads(self.params.get('LastGPSPositionLLK')) diff --git a/sunnypilot/selfdrive/locationd/torqued_ext.py b/openpilot/sunnypilot/selfdrive/locationd/torqued_ext.py similarity index 98% rename from sunnypilot/selfdrive/locationd/torqued_ext.py rename to openpilot/sunnypilot/selfdrive/locationd/torqued_ext.py index 1e62b7666a..f1f16f58ee 100644 --- a/sunnypilot/selfdrive/locationd/torqued_ext.py +++ b/openpilot/sunnypilot/selfdrive/locationd/torqued_ext.py @@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details. """ import numpy as np -from cereal import car +from opendbc.car.structs import car from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL diff --git a/sunnypilot/selfdrive/locationd/tests/__init__.py b/openpilot/sunnypilot/selfdrive/pandad/__init__.py similarity index 100% rename from sunnypilot/selfdrive/locationd/tests/__init__.py rename to openpilot/sunnypilot/selfdrive/pandad/__init__.py diff --git a/sunnypilot/selfdrive/pandad/rivian_long_flasher.py b/openpilot/sunnypilot/selfdrive/pandad/rivian_long_flasher.py similarity index 97% rename from sunnypilot/selfdrive/pandad/rivian_long_flasher.py rename to openpilot/sunnypilot/selfdrive/pandad/rivian_long_flasher.py index 305b994c78..3be9fb0e4d 100755 --- a/sunnypilot/selfdrive/pandad/rivian_long_flasher.py +++ b/openpilot/sunnypilot/selfdrive/pandad/rivian_long_flasher.py @@ -8,7 +8,8 @@ See the LICENSE.md file in the root directory for more details. import os from itertools import accumulate -from cereal import car, messaging +from openpilot.cereal import messaging +from opendbc.car.structs import car from panda import Panda from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog diff --git a/sunnypilot/selfdrive/pandad/rivian_long_fw.bin.signed b/openpilot/sunnypilot/selfdrive/pandad/rivian_long_fw.bin.signed similarity index 100% rename from sunnypilot/selfdrive/pandad/rivian_long_fw.bin.signed rename to openpilot/sunnypilot/selfdrive/pandad/rivian_long_fw.bin.signed diff --git a/sunnypilot/selfdrive/pandad/__init__.py b/openpilot/sunnypilot/selfdrive/selfdrived/__init__.py similarity index 100% rename from sunnypilot/selfdrive/pandad/__init__.py rename to openpilot/sunnypilot/selfdrive/selfdrived/__init__.py diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/openpilot/sunnypilot/selfdrive/selfdrived/events.py similarity index 97% rename from sunnypilot/selfdrive/selfdrived/events.py rename to openpilot/sunnypilot/selfdrive/selfdrived/events.py index 27b44fb4fc..2001d0dbee 100644 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ b/openpilot/sunnypilot/selfdrive/selfdrived/events.py @@ -4,13 +4,14 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -import cereal.messaging as messaging -from cereal import log, car, custom +import openpilot.cereal.messaging as messaging +from openpilot.cereal import log, custom +from opendbc.car.structs import car from openpilot.common.constants import CV from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ NoEntryAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, AlertCallbackType, wrong_car_mode_alert from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED, CONFIRM_SPEED_THRESHOLD -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE AlertSize = log.SelfdriveState.AlertSize AlertStatus = log.SelfdriveState.AlertStatus diff --git a/sunnypilot/selfdrive/selfdrived/events_base.py b/openpilot/sunnypilot/selfdrive/selfdrived/events_base.py similarity index 93% rename from sunnypilot/selfdrive/selfdrived/events_base.py rename to openpilot/sunnypilot/selfdrive/selfdrived/events_base.py index 5f9f8edf94..a392978144 100644 --- a/sunnypilot/selfdrive/selfdrived/events_base.py +++ b/openpilot/sunnypilot/selfdrive/selfdrived/events_base.py @@ -3,15 +3,16 @@ from enum import IntEnum from abc import abstractmethod from collections.abc import Callable -from cereal import log, car -import cereal.messaging as messaging +from openpilot.cereal import log +from opendbc.car.structs import car +import openpilot.cereal.messaging as messaging from openpilot.common.realtime import DT_CTRL -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE AlertSize = log.SelfdriveState.AlertSize AlertStatus = log.SelfdriveState.AlertStatus VisualAlert = car.CarControl.HUDControl.VisualAlert -AudibleAlert = car.CarControl.HUDControl.AudibleAlert +AudibleAlert = log.SelfdriveState.AudibleAlert # Alert priorities @@ -46,7 +47,7 @@ class Alert: alert_size: log.SelfdriveState.AlertSize, priority: Priority, visual_alert: car.CarControl.HUDControl.VisualAlert, - audible_alert: car.CarControl.HUDControl.AudibleAlert, + audible_alert: log.SelfdriveState.AudibleAlert, duration: float, creation_delay: float = 0.): @@ -77,7 +78,7 @@ class AlertBase(Alert): def __init__(self, alert_text_1: str, alert_text_2: str, alert_status: log.SelfdriveState.AlertStatus, alert_size: log.SelfdriveState.AlertSize, priority: Priority, visual_alert: car.CarControl.HUDControl.VisualAlert, - audible_alert: car.CarControl.HUDControl.AudibleAlert, duration: float): + audible_alert: log.SelfdriveState.AudibleAlert, duration: float): super().__init__(alert_text_1, alert_text_2, alert_status, alert_size, priority, visual_alert, audible_alert, duration) @@ -185,11 +186,12 @@ EmptyAlert = Alert("" , "", AlertStatus.normal, AlertSize.none, Priority.LOWEST, class NoEntryAlert(Alert): def __init__(self, alert_text_2: str, alert_text_1: str = "openpilot Unavailable", - visual_alert: car.CarControl.HUDControl.VisualAlert=VisualAlert.none): + visual_alert: car.CarControl.HUDControl.VisualAlert=VisualAlert.none, + priority: Priority = Priority.LOW): if HARDWARE.get_device_type() == 'mici': alert_text_1, alert_text_2 = alert_text_2, alert_text_1 super().__init__(alert_text_1, alert_text_2, AlertStatus.normal, - AlertSize.mid, Priority.LOW, visual_alert, + AlertSize.mid, priority, visual_alert, AudibleAlert.refuse, 3.) @@ -217,7 +219,7 @@ class ImmediateDisableAlert(Alert): class EngagementAlert(Alert): - def __init__(self, audible_alert: car.CarControl.HUDControl.AudibleAlert): + def __init__(self, audible_alert: log.SelfdriveState.AudibleAlert): super().__init__("", "", AlertStatus.normal, AlertSize.none, Priority.MID, VisualAlert.none, diff --git a/sunnypilot/selfdrive/ui/quiet_mode.py b/openpilot/sunnypilot/selfdrive/ui/quiet_mode.py similarity index 96% rename from sunnypilot/selfdrive/ui/quiet_mode.py rename to openpilot/sunnypilot/selfdrive/ui/quiet_mode.py index 739ea1392c..d4dba74d95 100644 --- a/sunnypilot/selfdrive/ui/quiet_mode.py +++ b/openpilot/sunnypilot/selfdrive/ui/quiet_mode.py @@ -4,7 +4,7 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import car +from opendbc.car.structs import car from openpilot.common.params import Params diff --git a/sunnypilot/selfdrive/selfdrived/__init__.py b/openpilot/sunnypilot/sunnylink/__init__.py similarity index 100% rename from sunnypilot/selfdrive/selfdrived/__init__.py rename to openpilot/sunnypilot/sunnylink/__init__.py diff --git a/sunnypilot/sunnylink/api.py b/openpilot/sunnypilot/sunnylink/api.py similarity index 95% rename from sunnypilot/sunnylink/api.py rename to openpilot/sunnypilot/sunnylink/api.py index a881faca04..7ca619721b 100644 --- a/sunnypilot/sunnylink/api.py +++ b/openpilot/sunnypilot/sunnylink/api.py @@ -8,8 +8,8 @@ from datetime import datetime, timedelta, UTC from openpilot.common.api.base import BaseApi from openpilot.common.params import Params -from openpilot.system.hardware import HARDWARE -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware import HARDWARE +from openpilot.common.hardware.hw import Paths API_HOST = os.getenv('SUNNYLINK_API_HOST', 'https://stg.api.sunnypilot.ai') UNREGISTERED_SUNNYLINK_DONGLE_ID = "UnregisteredDevice" @@ -47,16 +47,16 @@ class SunnylinkApi(BaseApi): return sunnylink_dongle_id, comma_dongle_id def _resolve_imeis(self): - imei1, imei2 = None, None + imei = None imei_try = 0 - while imei1 is None and imei2 is None and imei_try < MAX_RETRIES: + while imei is None and imei_try < MAX_RETRIES: try: - imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1) + imei = HARDWARE.get_imei() except Exception: self._status_update(f"Error getting imei, trying again... [{imei_try + 1}/{MAX_RETRIES}]") time.sleep(1) imei_try += 1 - return imei1, imei2 + return imei, "" def _resolve_serial(self): return (self.params.get("HardwareSerial") diff --git a/sunnypilot/sunnylink/__init__.py b/openpilot/sunnypilot/sunnylink/athena/__init__.py similarity index 100% rename from sunnypilot/sunnylink/__init__.py rename to openpilot/sunnypilot/sunnylink/athena/__init__.py diff --git a/sunnypilot/sunnylink/athena/manage_sunnylinkd.py b/openpilot/sunnypilot/sunnylink/athena/manage_sunnylinkd.py similarity index 76% rename from sunnypilot/sunnylink/athena/manage_sunnylinkd.py rename to openpilot/sunnypilot/sunnylink/athena/manage_sunnylinkd.py index 377b6990f7..d811fa5dbc 100755 --- a/sunnypilot/sunnylink/athena/manage_sunnylinkd.py +++ b/openpilot/sunnypilot/sunnylink/athena/manage_sunnylinkd.py @@ -2,4 +2,4 @@ from openpilot.system.athena.manage_athenad import manage_athenad if __name__ == '__main__': - manage_athenad("SunnylinkDongleId", "SunnylinkdPid", 'sunnylinkd', 'sunnypilot.sunnylink.athena.sunnylinkd') + manage_athenad("SunnylinkDongleId", "SunnylinkdPid", 'sunnylinkd', 'openpilot.sunnypilot.sunnylink.athena.sunnylinkd') diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py similarity index 91% rename from sunnypilot/sunnylink/athena/sunnylinkd.py rename to openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py index ce622bc5ad..ac168db7d8 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -16,18 +16,18 @@ import ssl import threading import time -from jsonrpc import dispatcher from functools import partial +from openpilot.system.athena.rpc import dispatcher from openpilot.common.params import Params, ParamKeyType from openpilot.common.realtime import set_core_affinity from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.system.athena.athenad import ws_send, jsonrpc_handler, \ recv_queue, UploadQueueCache, upload_queue, cur_upload_items, backoff, ws_manage, log_handler, start_local_proxy_shim, upload_handler, stat_handler from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException, create_connection, WebSocketConnectionClosedException) -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param from openpilot.sunnypilot.sunnylink.api import SunnylinkApi @@ -41,7 +41,6 @@ LOCAL_PORT_WHITELIST = {8022} SUNNYLINK_LOG_ATTR_NAME = "user.sunny.upload" SUNNYLINK_RECONNECT_TIMEOUT_S = 70 # FYI changing this will also would require a change on sidebar.cc DISALLOW_LOG_UPLOAD = threading.Event() -METADATA_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "params_metadata.json") params = Params() @@ -170,39 +169,6 @@ def getParamsAllKeys() -> list[str]: return keys -@dispatcher.add_method -def getParamsAllKeysV1() -> dict[str, str]: - try: - with open(METADATA_PATH) as f: - metadata = json.load(f) - except Exception: - cloudlog.exception("sunnylinkd.getParamsAllKeysV1.metadata.exception") - metadata = {} - - try: - available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()] - - params_dict: dict[str, list[dict[str, str | bool | int | object | dict | None]]] = {"params": []} - for key in available_keys: - value = get_param_as_byte(key, get_default=True) - - param_entry = { - "key": key, - "type": int(params.get_type(key).value), - "default_value": base64.b64encode(value).decode('utf-8') if value else None, - } - - if key in metadata: - meta_copy = metadata[key].copy() - param_entry["_extra"] = meta_copy - - params_dict["params"].append(param_entry) - return {"keys": json.dumps(params_dict.get("params", []))} - except Exception: - cloudlog.exception("sunnylinkd.getParamsAllKeysV1.exception") - raise - - @dispatcher.add_method def getParamsMetadata() -> str: """Return settings_ui.json + live capabilities as gzip-compressed, base64-encoded string. diff --git a/sunnypilot/sunnylink/athena/tests/test_sunnylinkd.py b/openpilot/sunnypilot/sunnylink/athena/tests/test_sunnylinkd.py similarity index 95% rename from sunnypilot/sunnylink/athena/tests/test_sunnylinkd.py rename to openpilot/sunnypilot/sunnylink/athena/tests/test_sunnylinkd.py index 616bff037e..7b81da5ad4 100644 --- a/sunnypilot/sunnylink/athena/tests/test_sunnylinkd.py +++ b/openpilot/sunnypilot/sunnylink/athena/tests/test_sunnylinkd.py @@ -16,10 +16,10 @@ class TestSunnylinkdMethods: def mock_save_param(key, value, compression=False): self.saved_params.append((key, value, compression)) - sunnylinkd.save_param_from_base64_encoded_string = mock_save_param + sunnylinkd.save_param_from_base64_encoded_string = mock_save_param # ty: ignore[invalid-assignment] def teardown_method(self): - sunnylinkd.save_param_from_base64_encoded_string = self.original_save + sunnylinkd.save_param_from_base64_encoded_string = self.original_save # ty: ignore[invalid-assignment] def test_saveParams_blocked(self): blocked_params = { diff --git a/sunnypilot/sunnylink/backups/AESCipher.py b/openpilot/sunnypilot/sunnylink/backups/AESCipher.py similarity index 100% rename from sunnypilot/sunnylink/backups/AESCipher.py rename to openpilot/sunnypilot/sunnylink/backups/AESCipher.py diff --git a/sunnypilot/sunnylink/athena/__init__.py b/openpilot/sunnypilot/sunnylink/backups/__init__.py similarity index 100% rename from sunnypilot/sunnylink/athena/__init__.py rename to openpilot/sunnypilot/sunnylink/backups/__init__.py diff --git a/sunnypilot/sunnylink/backups/manager.py b/openpilot/sunnypilot/sunnylink/backups/manager.py similarity index 99% rename from sunnypilot/sunnylink/backups/manager.py rename to openpilot/sunnypilot/sunnylink/backups/manager.py index 44cdb3bff2..5ac6b8cabc 100644 --- a/sunnypilot/sunnylink/backups/manager.py +++ b/openpilot/sunnypilot/sunnylink/backups/manager.py @@ -16,9 +16,9 @@ from openpilot.common.git import get_branch from openpilot.common.params import Params, ParamKeyFlag from openpilot.common.realtime import Ratekeeper from openpilot.common.swaglog import cloudlog -from openpilot.system.version import get_version +from openpilot.common.version import get_version -from cereal import messaging, custom +from openpilot.cereal import messaging, custom from openpilot.sunnypilot.sunnylink.api import SunnylinkApi from openpilot.sunnypilot.sunnylink.backups.utils import decrypt_compressed_data, encrypt_compressed_data, SnakeCaseEncoder from openpilot.sunnypilot.sunnylink.utils import get_param_as_byte, save_param_from_base64_encoded_string diff --git a/sunnypilot/sunnylink/backups/utils.py b/openpilot/sunnypilot/sunnylink/backups/utils.py similarity index 98% rename from sunnypilot/sunnylink/backups/utils.py rename to openpilot/sunnypilot/sunnylink/backups/utils.py index eb59205128..054a01793f 100644 --- a/sunnypilot/sunnylink/backups/utils.py +++ b/openpilot/sunnypilot/sunnylink/backups/utils.py @@ -18,7 +18,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa, ec from openpilot.common.api.base import KEYS from openpilot.sunnypilot.sunnylink.backups.AESCipher import AESCipher -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths class KeyDerivation: @@ -183,6 +183,6 @@ def transform_dict(obj): class SnakeCaseEncoder(json.JSONEncoder): - def encode(self, obj): - transformed_obj = transform_dict(obj) + def encode(self, o): + transformed_obj = transform_dict(o) return super().encode(transformed_obj) diff --git a/sunnypilot/sunnylink/capabilities.py b/openpilot/sunnypilot/sunnylink/capabilities.py similarity index 98% rename from sunnypilot/sunnylink/capabilities.py rename to openpilot/sunnypilot/sunnylink/capabilities.py index fbdcf8f04a..1f12b5b001 100644 --- a/sunnypilot/sunnylink/capabilities.py +++ b/openpilot/sunnypilot/sunnylink/capabilities.py @@ -6,13 +6,14 @@ See the LICENSE.md file in the root directory for more details. """ import json -from cereal import car, custom, messaging +from openpilot.cereal import custom, messaging +from opendbc.car.structs import car from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, UNSUPPORTED_LONGITUDINAL_CAR from opendbc.car.subaru.values import CAR as SUBARU_CAR, SubaruFlags from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE # Wire-protocol version for the capabilities payload. Bump on breaking changes diff --git a/sunnypilot/sunnylink/docs/README.md b/openpilot/sunnypilot/sunnylink/docs/README.md similarity index 98% rename from sunnypilot/sunnylink/docs/README.md rename to openpilot/sunnypilot/sunnylink/docs/README.md index 7327564ace..62d46677e6 100644 --- a/sunnypilot/sunnylink/docs/README.md +++ b/openpilot/sunnypilot/sunnylink/docs/README.md @@ -97,12 +97,11 @@ The compiler splices a list-context `$ref` into its parent list. Macros may refe ``` 1. common/params_keys.h — add/remove the C++ param key -2. params_metadata.json — automated via update_params_metadata.py -3. settings_ui_src/pages/.yaml — add/edit/remove the item in the right section -4. python sunnypilot/sunnylink/tools/compile_settings_ui.py -5. python sunnypilot/sunnylink/tools/validate_settings_ui.py (or: --check on the compiler) -6. uv run python -m pytest sunnypilot/sunnylink/tests/ # run regression + compiler tests -7. commit +2. settings_ui_src/pages/.yaml — add/edit/remove the item in the right section +3. python sunnypilot/sunnylink/tools/compile_settings_ui.py +4. python sunnypilot/sunnylink/tools/validate_settings_ui.py (or: --check on the compiler) +5. uv run python -m pytest sunnypilot/sunnylink/tests/ # run regression + compiler tests +6. commit ``` CI runs `compile_settings_ui.py --check` to fail on hand-edited `settings_ui.json`. diff --git a/sunnypilot/sunnylink/registration_manager.py b/openpilot/sunnypilot/sunnylink/registration_manager.py similarity index 95% rename from sunnypilot/sunnylink/registration_manager.py rename to openpilot/sunnypilot/sunnylink/registration_manager.py index 3e00c5c013..cc43dcae87 100755 --- a/sunnypilot/sunnylink/registration_manager.py +++ b/openpilot/sunnypilot/sunnylink/registration_manager.py @@ -5,7 +5,7 @@ from openpilot.common.params import Params from openpilot.common.realtime import Ratekeeper from openpilot.common.swaglog import cloudlog -from cereal import log, messaging +from openpilot.cereal import log, messaging from openpilot.sunnypilot.sunnylink.utils import register_sunnylink NetworkType = log.DeviceState.NetworkType diff --git a/sunnypilot/sunnylink/settings_ui.json b/openpilot/sunnypilot/sunnylink/settings_ui.json similarity index 100% rename from sunnypilot/sunnylink/settings_ui.json rename to openpilot/sunnypilot/sunnylink/settings_ui.json diff --git a/sunnypilot/sunnylink/settings_ui.schema.json b/openpilot/sunnypilot/sunnylink/settings_ui.schema.json similarity index 100% rename from sunnypilot/sunnylink/settings_ui.schema.json rename to openpilot/sunnypilot/sunnylink/settings_ui.schema.json diff --git a/sunnypilot/sunnylink/settings_ui_src/_macros.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/_macros.yaml similarity index 100% rename from sunnypilot/sunnylink/settings_ui_src/_macros.yaml rename to openpilot/sunnypilot/sunnylink/settings_ui_src/_macros.yaml diff --git a/sunnypilot/sunnylink/settings_ui_src/_schemas/macros.schema.json b/openpilot/sunnypilot/sunnylink/settings_ui_src/_schemas/macros.schema.json similarity index 100% rename from sunnypilot/sunnylink/settings_ui_src/_schemas/macros.schema.json rename to openpilot/sunnypilot/sunnylink/settings_ui_src/_schemas/macros.schema.json diff --git a/sunnypilot/sunnylink/settings_ui_src/_schemas/page.schema.json b/openpilot/sunnypilot/sunnylink/settings_ui_src/_schemas/page.schema.json similarity index 100% rename from sunnypilot/sunnylink/settings_ui_src/_schemas/page.schema.json rename to openpilot/sunnypilot/sunnylink/settings_ui_src/_schemas/page.schema.json diff --git a/sunnypilot/sunnylink/settings_ui_src/_schemas/rule.schema.json b/openpilot/sunnypilot/sunnylink/settings_ui_src/_schemas/rule.schema.json similarity index 100% rename from sunnypilot/sunnylink/settings_ui_src/_schemas/rule.schema.json rename to openpilot/sunnypilot/sunnylink/settings_ui_src/_schemas/rule.schema.json diff --git a/sunnypilot/sunnylink/settings_ui_src/pages/cruise.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/cruise.yaml similarity index 100% rename from sunnypilot/sunnylink/settings_ui_src/pages/cruise.yaml rename to openpilot/sunnypilot/sunnylink/settings_ui_src/pages/cruise.yaml diff --git a/sunnypilot/sunnylink/settings_ui_src/pages/developer.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/developer.yaml similarity index 100% rename from sunnypilot/sunnylink/settings_ui_src/pages/developer.yaml rename to openpilot/sunnypilot/sunnylink/settings_ui_src/pages/developer.yaml diff --git a/sunnypilot/sunnylink/settings_ui_src/pages/device.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/device.yaml similarity index 100% rename from sunnypilot/sunnylink/settings_ui_src/pages/device.yaml rename to openpilot/sunnypilot/sunnylink/settings_ui_src/pages/device.yaml diff --git a/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml similarity index 100% rename from sunnypilot/sunnylink/settings_ui_src/pages/display.yaml rename to openpilot/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml diff --git a/sunnypilot/sunnylink/settings_ui_src/pages/models.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/models.yaml similarity index 100% rename from sunnypilot/sunnylink/settings_ui_src/pages/models.yaml rename to openpilot/sunnypilot/sunnylink/settings_ui_src/pages/models.yaml diff --git a/sunnypilot/sunnylink/settings_ui_src/pages/software.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/software.yaml similarity index 100% rename from sunnypilot/sunnylink/settings_ui_src/pages/software.yaml rename to openpilot/sunnypilot/sunnylink/settings_ui_src/pages/software.yaml diff --git a/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml similarity index 100% rename from sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml rename to openpilot/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml diff --git a/sunnypilot/sunnylink/settings_ui_src/pages/toggles.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/toggles.yaml similarity index 100% rename from sunnypilot/sunnylink/settings_ui_src/pages/toggles.yaml rename to openpilot/sunnypilot/sunnylink/settings_ui_src/pages/toggles.yaml diff --git a/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml similarity index 100% rename from sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml rename to openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml diff --git a/sunnypilot/sunnylink/settings_ui_src/pages/visuals.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/visuals.yaml similarity index 100% rename from sunnypilot/sunnylink/settings_ui_src/pages/visuals.yaml rename to openpilot/sunnypilot/sunnylink/settings_ui_src/pages/visuals.yaml diff --git a/sunnypilot/sunnylink/statsd.py b/openpilot/sunnypilot/sunnylink/statsd.py similarity index 96% rename from sunnypilot/sunnylink/statsd.py rename to openpilot/sunnypilot/sunnylink/statsd.py index 233b531e85..7e8faf6327 100755 --- a/sunnypilot/sunnylink/statsd.py +++ b/openpilot/sunnypilot/sunnylink/statsd.py @@ -13,14 +13,14 @@ from collections import defaultdict from datetime import datetime, UTC from openpilot.common.params import Params -from cereal.messaging import SubMaster -from openpilot.system.hardware.hw import Paths +from openpilot.cereal.messaging import SubMaster +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.common.utils import atomic_write -from openpilot.system.version import get_build_metadata +from openpilot.common.version import get_build_metadata from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S -from openpilot.system.statsd import METRIC_TYPE, StatLogSP +from openpilot.sunnypilot.system.statsd import METRIC_TYPE, StatLogSP from openpilot.common.realtime import Ratekeeper STATSLOGSP = StatLogSP(intercept=False) diff --git a/sunnypilot/sunnylink/sunnylink_state.py b/openpilot/sunnypilot/sunnylink/sunnylink_state.py similarity index 99% rename from sunnypilot/sunnylink/sunnylink_state.py rename to openpilot/sunnypilot/sunnylink/sunnylink_state.py index a4a3df3a2b..7dc553951e 100644 --- a/sunnypilot/sunnylink/sunnylink_state.py +++ b/openpilot/sunnypilot/sunnylink/sunnylink_state.py @@ -11,7 +11,7 @@ import time import json import pyray as rl -from cereal import messaging +from openpilot.cereal import messaging from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID, SunnylinkApi diff --git a/sunnypilot/sunnylink/tests/test_capabilities.py b/openpilot/sunnypilot/sunnylink/tests/test_capabilities.py similarity index 100% rename from sunnypilot/sunnylink/tests/test_capabilities.py rename to openpilot/sunnypilot/sunnylink/tests/test_capabilities.py diff --git a/sunnypilot/sunnylink/tests/test_compile_settings_ui.py b/openpilot/sunnypilot/sunnylink/tests/test_compile_settings_ui.py similarity index 100% rename from sunnypilot/sunnylink/tests/test_compile_settings_ui.py rename to openpilot/sunnypilot/sunnylink/tests/test_compile_settings_ui.py diff --git a/sunnypilot/sunnylink/tests/test_settings_changes.py b/openpilot/sunnypilot/sunnylink/tests/test_settings_changes.py similarity index 100% rename from sunnypilot/sunnylink/tests/test_settings_changes.py rename to openpilot/sunnypilot/sunnylink/tests/test_settings_changes.py diff --git a/sunnypilot/sunnylink/tests/test_settings_schema.py b/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py similarity index 100% rename from sunnypilot/sunnylink/tests/test_settings_schema.py rename to openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py diff --git a/sunnypilot/sunnylink/tools/apply_macros.py b/openpilot/sunnypilot/sunnylink/tools/apply_macros.py similarity index 100% rename from sunnypilot/sunnylink/tools/apply_macros.py rename to openpilot/sunnypilot/sunnylink/tools/apply_macros.py diff --git a/sunnypilot/sunnylink/tools/compile_settings_ui.py b/openpilot/sunnypilot/sunnylink/tools/compile_settings_ui.py similarity index 100% rename from sunnypilot/sunnylink/tools/compile_settings_ui.py rename to openpilot/sunnypilot/sunnylink/tools/compile_settings_ui.py diff --git a/sunnypilot/sunnylink/tools/extract_settings_ui.py b/openpilot/sunnypilot/sunnylink/tools/extract_settings_ui.py similarity index 100% rename from sunnypilot/sunnylink/tools/extract_settings_ui.py rename to openpilot/sunnypilot/sunnylink/tools/extract_settings_ui.py diff --git a/sunnypilot/sunnylink/tools/generate_settings_schema.py b/openpilot/sunnypilot/sunnylink/tools/generate_settings_schema.py similarity index 100% rename from sunnypilot/sunnylink/tools/generate_settings_schema.py rename to openpilot/sunnypilot/sunnylink/tools/generate_settings_schema.py diff --git a/sunnypilot/sunnylink/tools/validate_settings_ui.py b/openpilot/sunnypilot/sunnylink/tools/validate_settings_ui.py similarity index 99% rename from sunnypilot/sunnylink/tools/validate_settings_ui.py rename to openpilot/sunnypilot/sunnylink/tools/validate_settings_ui.py index f1e094a742..a7bfe7cf67 100755 --- a/sunnypilot/sunnylink/tools/validate_settings_ui.py +++ b/openpilot/sunnypilot/sunnylink/tools/validate_settings_ui.py @@ -129,6 +129,10 @@ def validate_rule(rule: dict, path: str, result: ValidationResult, return False valid = True for i, cond in enumerate(rule["conditions"]): + if not isinstance(cond, dict): + result.error("rule well-formedness", f"{path}.{rule_type}[{i}]: condition must be a dict") + valid = False + continue if not validate_rule(cond, f"{path}.{rule_type}[{i}]", result, capability_fields): valid = False return valid diff --git a/sunnypilot/sunnylink/uploader.py b/openpilot/sunnypilot/sunnylink/uploader.py similarity index 98% rename from sunnypilot/sunnylink/uploader.py rename to openpilot/sunnypilot/sunnylink/uploader.py index 0b7eb78edb..358e3a88e7 100755 --- a/sunnypilot/sunnylink/uploader.py +++ b/openpilot/sunnypilot/sunnylink/uploader.py @@ -9,13 +9,13 @@ import traceback import datetime from collections.abc import Iterator -from cereal import log -import cereal.messaging as messaging +from openpilot.cereal import log +import openpilot.cereal.messaging as messaging from openpilot.sunnypilot.sunnylink.api import SunnylinkApi from openpilot.common.utils import get_upload_stream from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.system.loggerd.xattr_cache import getxattr, setxattr from openpilot.common.swaglog import cloudlog diff --git a/sunnypilot/sunnylink/utils.py b/openpilot/sunnypilot/sunnylink/utils.py similarity index 93% rename from sunnypilot/sunnylink/utils.py rename to openpilot/sunnypilot/sunnylink/utils.py index 89c3c8a0c8..59b2d15c12 100644 --- a/sunnypilot/sunnylink/utils.py +++ b/openpilot/sunnypilot/sunnylink/utils.py @@ -3,7 +3,7 @@ import gzip import json from openpilot.sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID from openpilot.common.params import Params, ParamKeyType -from openpilot.system.version import is_prebuilt +from openpilot.common.version import is_prebuilt def get_sunnylink_status(params=None) -> tuple[bool, bool, bool]: @@ -108,20 +108,22 @@ def _convert_param_to_type(value: bytes, param_type: ParamKeyType) -> bytes | st """ # We convert to string anything that isn't bytes first. We later transform further. - if param_type != ParamKeyType.BYTES: - value = value.decode('utf-8') + if param_type == ParamKeyType.BYTES: + return value + + decoded = value.decode('utf-8') if param_type == ParamKeyType.STRING: - value = value + return decoded elif param_type == ParamKeyType.BOOL: - value = value.lower() in ('true', '1', 'yes') + return decoded.lower() in ('true', '1', 'yes') elif param_type == ParamKeyType.INT: - value = int(value) + return int(decoded) elif param_type == ParamKeyType.FLOAT: - value = float(value) + return float(decoded) elif param_type == ParamKeyType.TIME: - value = str(value) + return str(decoded) elif param_type == ParamKeyType.JSON: - value = json.loads(value) + return json.loads(decoded) - return value + return decoded diff --git a/sunnypilot/sunnylink/backups/__init__.py b/openpilot/sunnypilot/system/__init__.py similarity index 100% rename from sunnypilot/sunnylink/backups/__init__.py rename to openpilot/sunnypilot/system/__init__.py diff --git a/sunnypilot/system/params_migration.py b/openpilot/sunnypilot/system/params_migration.py similarity index 100% rename from sunnypilot/system/params_migration.py rename to openpilot/sunnypilot/system/params_migration.py diff --git a/sunnypilot/system/sensord/.gitignore b/openpilot/sunnypilot/system/sensord/.gitignore similarity index 100% rename from sunnypilot/system/sensord/.gitignore rename to openpilot/sunnypilot/system/sensord/.gitignore diff --git a/sunnypilot/system/sensord/SConscript b/openpilot/sunnypilot/system/sensord/SConscript similarity index 100% rename from sunnypilot/system/sensord/SConscript rename to openpilot/sunnypilot/system/sensord/SConscript diff --git a/sunnypilot/system/sensord/sensors/bmx055_accel.cc b/openpilot/sunnypilot/system/sensord/sensors/bmx055_accel.cc similarity index 100% rename from sunnypilot/system/sensord/sensors/bmx055_accel.cc rename to openpilot/sunnypilot/system/sensord/sensors/bmx055_accel.cc diff --git a/sunnypilot/system/sensord/sensors/bmx055_accel.h b/openpilot/sunnypilot/system/sensord/sensors/bmx055_accel.h similarity index 100% rename from sunnypilot/system/sensord/sensors/bmx055_accel.h rename to openpilot/sunnypilot/system/sensord/sensors/bmx055_accel.h diff --git a/sunnypilot/system/sensord/sensors/bmx055_gyro.cc b/openpilot/sunnypilot/system/sensord/sensors/bmx055_gyro.cc similarity index 100% rename from sunnypilot/system/sensord/sensors/bmx055_gyro.cc rename to openpilot/sunnypilot/system/sensord/sensors/bmx055_gyro.cc diff --git a/sunnypilot/system/sensord/sensors/bmx055_gyro.h b/openpilot/sunnypilot/system/sensord/sensors/bmx055_gyro.h similarity index 100% rename from sunnypilot/system/sensord/sensors/bmx055_gyro.h rename to openpilot/sunnypilot/system/sensord/sensors/bmx055_gyro.h diff --git a/sunnypilot/system/sensord/sensors/bmx055_magn.cc b/openpilot/sunnypilot/system/sensord/sensors/bmx055_magn.cc similarity index 100% rename from sunnypilot/system/sensord/sensors/bmx055_magn.cc rename to openpilot/sunnypilot/system/sensord/sensors/bmx055_magn.cc diff --git a/sunnypilot/system/sensord/sensors/bmx055_magn.h b/openpilot/sunnypilot/system/sensord/sensors/bmx055_magn.h similarity index 100% rename from sunnypilot/system/sensord/sensors/bmx055_magn.h rename to openpilot/sunnypilot/system/sensord/sensors/bmx055_magn.h diff --git a/sunnypilot/system/sensord/sensors/bmx055_temp.cc b/openpilot/sunnypilot/system/sensord/sensors/bmx055_temp.cc similarity index 100% rename from sunnypilot/system/sensord/sensors/bmx055_temp.cc rename to openpilot/sunnypilot/system/sensord/sensors/bmx055_temp.cc diff --git a/sunnypilot/system/sensord/sensors/bmx055_temp.h b/openpilot/sunnypilot/system/sensord/sensors/bmx055_temp.h similarity index 100% rename from sunnypilot/system/sensord/sensors/bmx055_temp.h rename to openpilot/sunnypilot/system/sensord/sensors/bmx055_temp.h diff --git a/sunnypilot/system/sensord/sensors/constants.h b/openpilot/sunnypilot/system/sensord/sensors/constants.h similarity index 100% rename from sunnypilot/system/sensord/sensors/constants.h rename to openpilot/sunnypilot/system/sensord/sensors/constants.h diff --git a/sunnypilot/system/sensord/sensors/i2c_sensor.cc b/openpilot/sunnypilot/system/sensord/sensors/i2c_sensor.cc similarity index 100% rename from sunnypilot/system/sensord/sensors/i2c_sensor.cc rename to openpilot/sunnypilot/system/sensord/sensors/i2c_sensor.cc diff --git a/sunnypilot/system/sensord/sensors/i2c_sensor.h b/openpilot/sunnypilot/system/sensord/sensors/i2c_sensor.h similarity index 100% rename from sunnypilot/system/sensord/sensors/i2c_sensor.h rename to openpilot/sunnypilot/system/sensord/sensors/i2c_sensor.h diff --git a/sunnypilot/system/sensord/sensors/lsm6ds3_accel.cc b/openpilot/sunnypilot/system/sensord/sensors/lsm6ds3_accel.cc similarity index 100% rename from sunnypilot/system/sensord/sensors/lsm6ds3_accel.cc rename to openpilot/sunnypilot/system/sensord/sensors/lsm6ds3_accel.cc diff --git a/sunnypilot/system/sensord/sensors/lsm6ds3_accel.h b/openpilot/sunnypilot/system/sensord/sensors/lsm6ds3_accel.h similarity index 100% rename from sunnypilot/system/sensord/sensors/lsm6ds3_accel.h rename to openpilot/sunnypilot/system/sensord/sensors/lsm6ds3_accel.h diff --git a/sunnypilot/system/sensord/sensors/lsm6ds3_gyro.cc b/openpilot/sunnypilot/system/sensord/sensors/lsm6ds3_gyro.cc similarity index 100% rename from sunnypilot/system/sensord/sensors/lsm6ds3_gyro.cc rename to openpilot/sunnypilot/system/sensord/sensors/lsm6ds3_gyro.cc diff --git a/sunnypilot/system/sensord/sensors/lsm6ds3_gyro.h b/openpilot/sunnypilot/system/sensord/sensors/lsm6ds3_gyro.h similarity index 100% rename from sunnypilot/system/sensord/sensors/lsm6ds3_gyro.h rename to openpilot/sunnypilot/system/sensord/sensors/lsm6ds3_gyro.h diff --git a/sunnypilot/system/sensord/sensors/lsm6ds3_temp.cc b/openpilot/sunnypilot/system/sensord/sensors/lsm6ds3_temp.cc similarity index 100% rename from sunnypilot/system/sensord/sensors/lsm6ds3_temp.cc rename to openpilot/sunnypilot/system/sensord/sensors/lsm6ds3_temp.cc diff --git a/sunnypilot/system/sensord/sensors/lsm6ds3_temp.h b/openpilot/sunnypilot/system/sensord/sensors/lsm6ds3_temp.h similarity index 100% rename from sunnypilot/system/sensord/sensors/lsm6ds3_temp.h rename to openpilot/sunnypilot/system/sensord/sensors/lsm6ds3_temp.h diff --git a/sunnypilot/system/sensord/sensors/mmc5603nj_magn.cc b/openpilot/sunnypilot/system/sensord/sensors/mmc5603nj_magn.cc similarity index 100% rename from sunnypilot/system/sensord/sensors/mmc5603nj_magn.cc rename to openpilot/sunnypilot/system/sensord/sensors/mmc5603nj_magn.cc diff --git a/sunnypilot/system/sensord/sensors/mmc5603nj_magn.h b/openpilot/sunnypilot/system/sensord/sensors/mmc5603nj_magn.h similarity index 100% rename from sunnypilot/system/sensord/sensors/mmc5603nj_magn.h rename to openpilot/sunnypilot/system/sensord/sensors/mmc5603nj_magn.h diff --git a/sunnypilot/system/sensord/sensors/sensor.h b/openpilot/sunnypilot/system/sensord/sensors/sensor.h similarity index 100% rename from sunnypilot/system/sensord/sensors/sensor.h rename to openpilot/sunnypilot/system/sensord/sensors/sensor.h diff --git a/sunnypilot/system/sensord/sensors_qcom2.cc b/openpilot/sunnypilot/system/sensord/sensors_qcom2.cc similarity index 100% rename from sunnypilot/system/sensord/sensors_qcom2.cc rename to openpilot/sunnypilot/system/sensord/sensors_qcom2.cc diff --git a/sunnypilot/system/sensord/tests/test_sensord.py b/openpilot/sunnypilot/system/sensord/tests/test_sensord.py similarity index 95% rename from sunnypilot/system/sensord/tests/test_sensord.py rename to openpilot/sunnypilot/system/sensord/tests/test_sensord.py index dc5f93d97b..53f5d27219 100644 --- a/sunnypilot/system/sensord/tests/test_sensord.py +++ b/openpilot/sunnypilot/system/sensord/tests/test_sensord.py @@ -1,15 +1,16 @@ import os +import subprocess import pytest import time import numpy as np from collections import namedtuple, defaultdict -import cereal.messaging as messaging -from cereal import log -from cereal.services import SERVICE_LIST +import openpilot.cereal.messaging as messaging +from openpilot.cereal import log +from openpilot.cereal.services import SERVICE_LIST from openpilot.common.gpio import get_irqs_for_action from openpilot.common.timeout import Timeout -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.system.manager.process_config import managed_processes BMX = { @@ -70,7 +71,7 @@ ALL_SENSORS = { } -def get_irq_count(irq: int): +def get_irq_count(irq: str): with open(f"/sys/kernel/irq/{irq}/per_cpu_count") as f: per_cpu = map(int, f.read().split(",")) return sum(per_cpu) @@ -110,7 +111,7 @@ class TestSensord: os.environ["LSM_SELF_TEST"] = "1" # read initial sensor values every test case can use - os.system("pkill -f \\\\./sensord") + subprocess.run(["pkill", "-f", r"\\./sensord"], check=False) try: managed_processes["sensord"].start() cls.sample_secs = int(os.getenv("SAMPLE_SECS", "10")) @@ -180,7 +181,7 @@ class TestSensord: def test_logmonottime_timestamp_diff(self): # ensure diff between the message logMonotime and sample timestamp is small - tdiffs = list() + tdiffs = [] for etype in self.events: for measurement in self.events[etype]: m = getattr(measurement, measurement.which()) @@ -202,7 +203,7 @@ class TestSensord: assert avg_diff < 4, f"Avg packet diff: {avg_diff:.1f}ms" def test_sensor_values(self): - sensor_values = dict() + sensor_values = {} for etype in self.events: for measurement in self.events[etype]: m = getattr(measurement, measurement.which()) diff --git a/sunnypilot/system/sensord/tests/ttff_test.py b/openpilot/sunnypilot/system/sensord/tests/ttff_test.py similarity index 96% rename from sunnypilot/system/sensord/tests/ttff_test.py rename to openpilot/sunnypilot/system/sensord/tests/ttff_test.py index a9cc16d707..cc8408055c 100755 --- a/sunnypilot/system/sensord/tests/ttff_test.py +++ b/openpilot/sunnypilot/system/sensord/tests/ttff_test.py @@ -3,7 +3,7 @@ import time import atexit -from cereal import messaging +from openpilot.cereal import messaging from openpilot.system.manager.process_config import managed_processes TIMEOUT = 10*60 diff --git a/system/statsd.py b/openpilot/sunnypilot/system/statsd.py similarity index 97% rename from system/statsd.py rename to openpilot/sunnypilot/system/statsd.py index 3a67dd44c2..ff77a5a57f 100755 --- a/system/statsd.py +++ b/openpilot/sunnypilot/system/statsd.py @@ -13,12 +13,12 @@ from datetime import datetime, UTC, date from typing import NoReturn from openpilot.common.params import Params -from cereal.messaging import SubMaster -from openpilot.system.hardware.hw import Paths +from openpilot.cereal.messaging import SubMaster +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.common.utils import atomic_write -from openpilot.system.version import get_build_metadata +from openpilot.common.version import get_build_metadata from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S diff --git a/sunnypilot/system/updated/tests/test_sp_branch_migrations.py b/openpilot/sunnypilot/system/updated/tests/test_sp_branch_migrations.py similarity index 100% rename from sunnypilot/system/updated/tests/test_sp_branch_migrations.py rename to openpilot/sunnypilot/system/updated/tests/test_sp_branch_migrations.py diff --git a/sunnypilot/system/__init__.py b/openpilot/sunnypilot/tools/__init__.py similarity index 100% rename from sunnypilot/system/__init__.py rename to openpilot/sunnypilot/tools/__init__.py diff --git a/sunnypilot/tools/__init__.py b/openpilot/sunnypilot/tools/memory_profiler/__init__.py similarity index 100% rename from sunnypilot/tools/__init__.py rename to openpilot/sunnypilot/tools/memory_profiler/__init__.py diff --git a/sunnypilot/tools/memory_profiler/mem_usage.py b/openpilot/sunnypilot/tools/memory_profiler/mem_usage.py similarity index 100% rename from sunnypilot/tools/memory_profiler/mem_usage.py rename to openpilot/sunnypilot/tools/memory_profiler/mem_usage.py diff --git a/sunnypilot/tools/pull_footage.py b/openpilot/sunnypilot/tools/pull_footage.py similarity index 100% rename from sunnypilot/tools/pull_footage.py rename to openpilot/sunnypilot/tools/pull_footage.py diff --git a/openpilot/system b/openpilot/system deleted file mode 120000 index 16f8cc2b23..0000000000 --- a/openpilot/system +++ /dev/null @@ -1 +0,0 @@ -../system/ \ No newline at end of file diff --git a/sunnypilot/tools/memory_profiler/__init__.py b/openpilot/system/__init__.py similarity index 100% rename from sunnypilot/tools/memory_profiler/__init__.py rename to openpilot/system/__init__.py diff --git a/system/__init__.py b/openpilot/system/athena/__init__.py similarity index 100% rename from system/__init__.py rename to openpilot/system/athena/__init__.py diff --git a/system/athena/athenad.py b/openpilot/system/athena/athenad.py similarity index 91% rename from system/athena/athenad.py rename to openpilot/system/athena/athenad.py index 9b3262ad99..1f59c8d271 100755 --- a/system/athena/athenad.py +++ b/openpilot/system/athena/athenad.py @@ -3,7 +3,7 @@ from __future__ import annotations import base64 import hashlib -import io +import itertools import json import os import queue @@ -24,28 +24,28 @@ from collections.abc import Callable import requests from requests.adapters import HTTPAdapter, DEFAULT_POOLBLOCK -from jsonrpc import JSONRPCResponseManager, dispatcher from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException, create_connection) -import cereal.messaging as messaging -from cereal import car, log -from cereal.services import SERVICE_LIST +import openpilot.cereal.messaging as messaging +from openpilot.cereal import log +from opendbc.car.structs import car +from openpilot.cereal.services import SERVICE_LIST from openpilot.common.api import Api, get_key_pair from openpilot.common.utils import CallbackReader, get_upload_stream from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity -from openpilot.system.hardware import HARDWARE, PC +from openpilot.common.hardware import HARDWARE, PC from openpilot.system.loggerd.xattr_cache import getxattr, setxattr from openpilot.common.swaglog import cloudlog -from openpilot.system.version import get_build_metadata -from openpilot.system.hardware.hw import Paths +from openpilot.common.version import get_build_metadata +from openpilot.common.hardware.hw import Paths +from openpilot.system.athena.rpc import dispatcher, handle, is_call, is_response, loads ATHENA_HOST = os.getenv('ATHENA_HOST', 'wss://athena.comma.ai') HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4")) LOCAL_PORT_WHITELIST = {22, } # SSH -WEBRTCD_PORT = 5001 LOG_ATTR_NAME = 'user.upload' LOG_ATTR_VALUE_MAX_UNIX_TIME = int.to_bytes(2147483647, 4, sys.byteorder) @@ -58,6 +58,9 @@ WS_FRAME_SIZE = 4096 DEVICE_STATE_UPDATE_INTERVAL = 1.0 # in seconds DEFAULT_UPLOAD_PRIORITY = 99 # higher number = lower priority +SEND_PRIORITY_HIGH = 0 +SEND_PRIORITY_LOW = 1 + # https://bytesolutions.com/dscp-tos-cos-precedence-conversion-chart, # https://en.wikipedia.org/wiki/Differentiated_services UPLOAD_TOS = 0x20 # CS1, low priority background traffic @@ -127,14 +130,18 @@ class UploadItem: dispatcher["echo"] = lambda s: s recv_queue: Queue[str] = queue.Queue() -send_queue: Queue[str] = queue.Queue() +send_queue: Queue[tuple[int, int, str]] = queue.PriorityQueue() upload_queue: Queue[UploadItem] = queue.PriorityQueue() -low_priority_send_queue: Queue[str] = queue.Queue() log_recv_queue: Queue[str] = queue.Queue() cancelled_uploads: set[str] = set() cur_upload_items: dict[int, UploadItem | None] = {} +send_seq = itertools.count() +def send_queue_push(data: str, priority: int) -> None: + assert priority is not None, "send queue priority must be specified" + send_queue.put_nowait((priority, next(send_seq), data)) # tie-break with a monotonic counter + # TODO-SP: adapt zst for sunnylink def strip_zst_extension(fn: str) -> str: @@ -207,11 +214,11 @@ def jsonrpc_handler(end_event: threading.Event, localProxyHandler = None) -> Non while not end_event.is_set(): try: data = recv_queue.get(timeout=1) - if "method" in data: + msg = loads(data) + if is_call(msg): cloudlog.event("athena.jsonrpc_handler.call_method", data=data) - response = JSONRPCResponseManager.handle(data, dispatcher) - send_queue.put_nowait(response.json) - elif "id" in data and ("result" in data or "error" in data): + send_queue_push(handle(msg, dispatcher), SEND_PRIORITY_HIGH) + elif is_response(msg): log_recv_queue.put_nowait(data) else: raise Exception("not a valid request or response") @@ -219,7 +226,7 @@ def jsonrpc_handler(end_event: threading.Event, localProxyHandler = None) -> Non pass except Exception as e: cloudlog.exception("athena jsonrpc handler failed") - send_queue.put_nowait(json.dumps({"error": str(e)})) + send_queue_push(json.dumps({"error": str(e)}), SEND_PRIORITY_HIGH) def retry_upload(tid: int, end_event: threading.Event, increase_count: bool = True) -> None: @@ -595,55 +602,28 @@ def getNetworkMetered() -> bool: @dispatcher.add_method -def getNetworks(): - return HARDWARE.get_networks() - - -@dispatcher.add_method -def startStream(sdp: str) -> dict: - from openpilot.system.webrtc.webrtcd import StreamRequestBody +def startStream(sdp: str, enabled: bool) -> dict: + from openpilot.system.webrtc.helpers import StreamRequestBody, post_stream_request, wait_for_webrtcd + params = Params() bridge_services_in = [] - # get live car params to avoid stale notCar edge case - cp_bytes = Params().get("CarParams") + # stale car params case taken care of by webrtcd being shut off on ignition + cp_bytes = Params().get("CarParamsPersistent") if cp_bytes is not None: with car.CarParams.from_bytes(cp_bytes) as CP: if CP.notCar: bridge_services_in.append("testJoystick") - - body = StreamRequestBody(sdp, "wideRoad", bridge_services_in, ["carState"]) - try: - resp = requests.post(f"http://localhost:{WEBRTCD_PORT}/stream", - json=asdict(body), timeout=10) - if not resp.ok: - try: - error_body = resp.json() - raise Exception(error_body.get("message", f"webrtcd returned {resp.status_code}")) - except ValueError: - resp.raise_for_status() - return resp.json() - except requests.ConnectTimeout as e: - raise Exception("webrtc took too long to respond. is it on?") from e - except requests.ConnectionError as e: - raise Exception("webrtc is not running. turn on comma body ignition.") from e - - -@dispatcher.add_method -def takeSnapshot() -> str | dict[str, str] | None: - from openpilot.system.camerad.snapshot import jpeg_write, snapshot - ret = snapshot() - if ret is not None: - def b64jpeg(x): - if x is not None: - f = io.BytesIO() - jpeg_write(f, x) - return base64.b64encode(f.getvalue()).decode("utf-8") - else: - return None - return {'jpegBack': b64jpeg(ret[0]), - 'jpegFront': b64jpeg(ret[1])} else: - raise Exception("not available while camerad is started") + raise Exception("failed to get CarParamsPersistent") + + if params.get_bool("IsOffroad"): + # manager owns camerad/stream_encoderd/webrtcd; flip the param and let it bring them up. + # webrtcd clears IsLiveStreaming when the session ends + params.put_bool("IsLiveStreaming", True) + # wait for webrtcd end points to wake up + wait_for_webrtcd() + + return post_stream_request(StreamRequestBody(sdp, "wideRoad", enabled, bridge_services_in, ["carState", "deviceState"])) def get_logs_to_send_sorted(log_attr_name=LOG_ATTR_NAME) -> list[str]: @@ -699,29 +679,28 @@ def add_log_to_queue(log_path, log_id, is_sunnylink=False): f"after compression: {compressed_size} bytes, " + f"after encoding: {encoded_size} bytes") - jsonrpc = { + params: dict[str, str | bool] = {"logs": payload} + if is_sunnylink and is_compressed: + params["compressed"] = is_compressed + + jsonrpc: dict = { "method": "forwardLogs", - "params": { - "logs": payload - }, + "params": params, "jsonrpc": "2.0", "id": log_id } - if is_sunnylink and is_compressed: - jsonrpc["params"]["compressed"] = is_compressed - jsonrpc_str = json.dumps(jsonrpc) size_in_bytes = len(jsonrpc_str.encode('utf-8')) if is_sunnylink and size_in_bytes <= MAX_SIZE_BYTES: cloudlog.debug(f"Target is sunnylink and log file {log_path} is small enough to send in one request ({size_in_bytes} bytes).") - low_priority_send_queue.put_nowait(jsonrpc_str) + send_queue_push(jsonrpc_str, SEND_PRIORITY_LOW) elif is_sunnylink: cloudlog.warning(f"Target is sunnylink and log file {log_path} is too large to send in one request.") else: cloudlog.debug(f"Target is not sunnylink, proceeding to send log file {log_path} in one request ({size_in_bytes} bytes).") - low_priority_send_queue.put_nowait(jsonrpc_str) + send_queue_push(jsonrpc_str, SEND_PRIORITY_LOW) def log_handler(end_event: threading.Event, log_attr_name=LOG_ATTR_NAME) -> None: @@ -803,19 +782,18 @@ def stat_handler(end_event: threading.Event, stats_dir=None, is_sunnylink=False) payload = base64.b64encode(compressed_data).decode() is_compressed = True - jsonrpc = { + params: dict[str, str | bool] = {"stats": payload} + if is_sunnylink and is_compressed: + params["compressed"] = is_compressed + + jsonrpc: dict = { "method": "storeStats", - "params": { - "stats": payload - }, + "params": params, "jsonrpc": "2.0", "id": stat_filenames[0] } - if is_sunnylink and is_compressed: - jsonrpc["params"]["compressed"] = is_compressed - - low_priority_send_queue.put_nowait(json.dumps(jsonrpc)) + send_queue_push(json.dumps(jsonrpc), SEND_PRIORITY_LOW) os.remove(stat_path) last_scan = curr_scan except Exception: @@ -826,7 +804,10 @@ def stat_handler(end_event: threading.Event, stats_dir=None, is_sunnylink=False) def ws_proxy_recv(ws: WebSocket, local_sock: socket.socket, ssock: socket.socket, end_event: threading.Event, global_end_event: threading.Event) -> None: while not (end_event.is_set() or global_end_event.is_set()): try: - r = select.select((ws.sock,), (), (), 30) + sock = ws.sock + if sock is None: + return + r = select.select((sock,), (), (), 30) if r[0]: data = ws.recv() if isinstance(data, str): @@ -897,10 +878,7 @@ def ws_recv(ws: WebSocket, end_event: threading.Event) -> None: def ws_send(ws: WebSocket, end_event: threading.Event) -> None: while not end_event.is_set(): try: - try: - data = send_queue.get_nowait() - except queue.Empty: - data = low_priority_send_queue.get(timeout=1) + _, _, data = send_queue.get(timeout=1) for i in range(0, len(data), WS_FRAME_SIZE): frame = data[i:i+WS_FRAME_SIZE] last = i + WS_FRAME_SIZE >= len(data) @@ -919,7 +897,7 @@ def ws_manage(ws: WebSocket, end_event: threading.Event) -> None: sock = ws.sock while not end_event.wait(5): - onroad = params.get_bool("IsOnroad") + onroad = not params.get_bool("IsOffroad") if onroad != onroad_prev: onroad_prev = onroad diff --git a/system/athena/manage_athenad.py b/openpilot/system/athena/manage_athenad.py similarity index 86% rename from system/athena/manage_athenad.py rename to openpilot/system/athena/manage_athenad.py index 98557db06a..3f0d6adcd3 100755 --- a/system/athena/manage_athenad.py +++ b/openpilot/system/athena/manage_athenad.py @@ -6,14 +6,14 @@ from multiprocessing import Process from openpilot.common.params import Params from openpilot.system.manager.process import launcher from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import HARDWARE -from openpilot.system.version import get_build_metadata +from openpilot.common.hardware import HARDWARE +from openpilot.common.version import get_build_metadata ATHENA_MGR_PID_PARAM = "AthenadPid" def main(): - manage_athenad("DongleId", ATHENA_MGR_PID_PARAM, 'athenad', 'system.athena.athenad') + manage_athenad("DongleId", ATHENA_MGR_PID_PARAM, 'athenad', 'openpilot.system.athena.athenad') def manage_athenad(dongle_id_param, pid_param, process_name, target): diff --git a/system/athena/registration.py b/openpilot/system/athena/registration.py similarity index 87% rename from system/athena/registration.py rename to openpilot/system/athena/registration.py index bc7cf6c748..5e80fe5598 100755 --- a/system/athena/registration.py +++ b/openpilot/system/athena/registration.py @@ -10,8 +10,8 @@ from openpilot.common.api import api_get, get_key_pair from openpilot.common.params import Params from openpilot.common.spinner import Spinner from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert -from openpilot.system.hardware import HARDWARE, PC -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware import HARDWARE, PC +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog @@ -54,17 +54,16 @@ def register(show_spinner=False) -> str | None: # Block until we get the imei serial = HARDWARE.get_serial() start_time = time.monotonic() - imei1: str | None = None - imei2: str | None = None - while imei1 is None and imei2 is None: + imei: str | None = None + while imei is None: try: - imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1) + imei = HARDWARE.get_imei() except Exception: cloudlog.exception("Error getting imei, trying again...") time.sleep(1) if time.monotonic() - start_time > 60 and show_spinner: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") + spinner.update(f"registering device - serial: {serial}, IMEI: {imei}") backoff = 0 start_time = time.monotonic() @@ -75,7 +74,7 @@ def register(show_spinner=False) -> str | None: cloudlog.info("getting pilotauth") cloudlog.info("getting pilotauth") resp = api_get("v2/pilotauth/", method='POST', timeout=15, - imei=imei1, imei2=imei2, serial=serial, public_key=public_key, register_token=register_token) + imei=imei, imei2="", serial=serial, public_key=public_key, register_token=register_token) if resp.status_code in (402, 403): cloudlog.info(f"Unable to register device, got {resp.status_code}") @@ -84,13 +83,16 @@ def register(show_spinner=False) -> str | None: dongleauth = json.loads(resp.text) dongle_id = dongleauth["dongle_id"] break + except NotImplementedError: + # dependency issues with PyJWT will hang the registration test in backoff loop otherwise + raise except Exception: cloudlog.exception("failed to authenticate") backoff = min(backoff + 1, 15) time.sleep(backoff) if time.monotonic() - start_time > 60 and show_spinner: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") + spinner.update(f"registering device - serial: {serial}, IMEI: {imei}") return UNREGISTERED_DONGLE_ID # hotfix to prevent an infinite wait for registration if show_spinner: diff --git a/openpilot/system/athena/rpc.py b/openpilot/system/athena/rpc.py new file mode 100644 index 0000000000..aaf7f41fc3 --- /dev/null +++ b/openpilot/system/athena/rpc.py @@ -0,0 +1,116 @@ +import json +from collections.abc import Callable, Mapping +from typing import Any + +# a minimal implementation of json-rpc 2.0 https://www.jsonrpc.org/specification + +JSONRPC_VERSION = "2.0" + +# JSON-RPC 2.0 reserved / application error codes +PARSE_ERROR = -32700 +INVALID_REQUEST = -32600 +METHOD_NOT_FOUND = -32601 +INVALID_PARAMS = -32602 +SERVER_ERROR = -32000 + +JsonDict = dict[str, Any] +MethodMap = Mapping[str, Callable[..., Any]] + + +class Dispatcher(dict[str, Callable[..., Any]]): + def add_method(self, f: Callable[..., Any] | None = None, *, name: str | None = None): + def decorator(fn: Callable[..., Any]) -> Callable[..., Any]: + self[name or fn.__name__] = fn + return fn + return decorator(f) if f is not None else decorator + + +dispatcher = Dispatcher() + + +def dumps_call(method: str, params: Any = None, request_id: Any = None) -> str: + msg: JsonDict = {"jsonrpc": JSONRPC_VERSION, "method": method, "id": request_id} + if params is not None: + msg["params"] = params + return json.dumps(msg) + + +def dumps_result(request_id: Any, result: Any) -> str: + return json.dumps({"jsonrpc": JSONRPC_VERSION, "id": request_id, "result": result}) + + +def dumps_error(request_id: Any, message: str, code: int = SERVER_ERROR) -> str: + return json.dumps({ + "jsonrpc": JSONRPC_VERSION, + "id": request_id, + "error": {"code": code, "message": message}, + }) + + +def loads(raw: str | bytes) -> JsonDict: + if isinstance(raw, bytes): + raw = raw.decode() + data = json.loads(raw) + if not isinstance(data, dict): + raise ValueError("message must be a JSON object") + return data + + +def is_call(msg: JsonDict) -> bool: + return "method" in msg + + +def is_response(msg: JsonDict) -> bool: + return "id" in msg and ("result" in msg or "error" in msg) + + +def error_message(err: Any) -> str: + """Normalize JSON-RPC object errors and plain-string errors.""" + if isinstance(err, str): + return err + if isinstance(err, dict): + data = err.get("data") + if isinstance(data, dict) and data.get("message"): + return str(data["message"]) + if err.get("message") is not None: + return str(err["message"]) + return str(err) + + +def _invoke(fn: Callable[..., Any], params: Any) -> Any: + if params is None: + return fn() + if isinstance(params, dict): + return fn(**params) + if isinstance(params, (list, tuple)): + return fn(*params) + raise TypeError("params must be a list, object, or omitted") + + +def handle(raw: str | bytes | JsonDict, methods: MethodMap | None = None) -> str: + methods = dispatcher if methods is None else methods + + try: + msg = raw if isinstance(raw, dict) else loads(raw) + except (TypeError, ValueError, UnicodeDecodeError): + return dumps_error(None, "parse error", PARSE_ERROR) + + if not is_call(msg): + raise ValueError("not a call") + + req_id = msg.get("id") + name = msg.get("method") + if not isinstance(name, str): + return dumps_error(req_id, "invalid request", INVALID_REQUEST) + + try: + fn = methods[name] + except KeyError: + return dumps_error(req_id, f"method not found: {name}", METHOD_NOT_FOUND) + + try: + return dumps_result(req_id, _invoke(fn, msg.get("params"))) + except TypeError as e: + return dumps_error(req_id, str(e), INVALID_PARAMS) + except Exception as e: + return dumps_error(req_id, str(e), SERVER_ERROR) diff --git a/system/athena/__init__.py b/openpilot/system/athena/tests/__init__.py similarity index 100% rename from system/athena/__init__.py rename to openpilot/system/athena/tests/__init__.py diff --git a/system/athena/tests/helpers.py b/openpilot/system/athena/tests/helpers.py similarity index 100% rename from system/athena/tests/helpers.py rename to openpilot/system/athena/tests/helpers.py diff --git a/system/athena/tests/test_athenad.py b/openpilot/system/athena/tests/test_athenad.py similarity index 97% rename from system/athena/tests/test_athenad.py rename to openpilot/system/athena/tests/test_athenad.py index b0c20a26a9..8c090b93af 100644 --- a/system/athena/tests/test_athenad.py +++ b/openpilot/system/athena/tests/test_athenad.py @@ -14,7 +14,7 @@ from datetime import datetime, timedelta from websocket import ABNF from websocket._exceptions import WebSocketConnectionClosedException -from cereal import messaging +from openpilot.cereal import messaging from openpilot.common.params import Params from openpilot.common.timeout import Timeout @@ -22,7 +22,7 @@ from openpilot.system.athena import athenad from openpilot.system.athena.athenad import MAX_RETRY_COUNT, UPLOAD_SESS, dispatcher from openpilot.system.athena.tests.helpers import HTTPRequestHandler, MockWebsocket, MockApi, EchoSocket from openpilot.selfdrive.test.helpers import http_server_context -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths def seed_athena_server(host, port): @@ -60,7 +60,7 @@ class TestAthenadMethods: @classmethod def setup_class(cls): cls.SOCKET_PORT = 45454 - athenad.Api = MockApi + athenad.Api = MockApi # ty: ignore[invalid-assignment] # test double athenad.LOCAL_PORT_WHITELIST = {cls.SOCKET_PORT} def setup_method(self): @@ -351,6 +351,7 @@ class TestAthenadMethods: assert items[0] == asdict(item) assert not items[0]['current'] + assert item.id is not None athenad.cancelled_uploads.add(item.id) items = dispatcher["listUploadQueue"]() assert len(items) == 0 @@ -363,6 +364,7 @@ class TestAthenadMethods: athenad.upload_queue.put_nowait(item2) # Ensure canceled items are not persisted + assert item2.id is not None athenad.cancelled_uploads.add(item2.id) # serialize item @@ -422,11 +424,11 @@ class TestAthenadMethods: try: # with params athenad.recv_queue.put_nowait(json.dumps({"method": "echo", "params": ["hello"], "jsonrpc": "2.0", "id": 0})) - resp = athenad.send_queue.get(timeout=3) + _, _, resp = athenad.send_queue.get(timeout=3) assert json.loads(resp) == {'result': 'hello', 'id': 0, 'jsonrpc': '2.0'} # without params athenad.recv_queue.put_nowait(json.dumps({"method": "getNetworkType", "jsonrpc": "2.0", "id": 0})) - resp = athenad.send_queue.get(timeout=3) + _, _, resp = athenad.send_queue.get(timeout=3) assert json.loads(resp) == {'result': 1, 'id': 0, 'jsonrpc': '2.0'} # log forwarding athenad.recv_queue.put_nowait(json.dumps({'result': {'success': 1}, 'id': 0, 'jsonrpc': '2.0'})) @@ -437,7 +439,7 @@ class TestAthenadMethods: thread.join() def test_get_logs_to_send_sorted(self): - fl = list() + fl = [] for i in range(10): file = f'swaglog.{i:010}' self._create_file(file, Paths.swaglog_root()) diff --git a/system/athena/tests/test_athenad_ping.py b/openpilot/system/athena/tests/test_athenad_ping.py similarity index 94% rename from system/athena/tests/test_athenad_ping.py rename to openpilot/system/athena/tests/test_athenad_ping.py index 8ff1e37a5d..44a6b8a56b 100644 --- a/system/athena/tests/test_athenad_ping.py +++ b/openpilot/system/athena/tests/test_athenad_ping.py @@ -7,8 +7,7 @@ from typing import cast from openpilot.common.params import Params from openpilot.common.timeout import Timeout from openpilot.system.athena import athenad -from openpilot.system.manager.helpers import write_onroad_params -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI TIMEOUT_TOLERANCE = 20 # seconds @@ -93,10 +92,10 @@ class TestAthenadPing: @pytest.mark.skipif(not TICI, reason="only run on desk") def test_offroad(self, subtests, mocker) -> None: - write_onroad_params(False, self.params) + self.params.put_bool("IsOffroad", True, block=True) self.assertTimeout(60 + TIMEOUT_TOLERANCE, subtests, mocker) # based using TCP keepalive settings @pytest.mark.skipif(not TICI, reason="only run on desk") def test_onroad(self, subtests, mocker) -> None: - write_onroad_params(True, self.params) + self.params.put_bool("IsOffroad", False, block=True) self.assertTimeout(21 + TIMEOUT_TOLERANCE, subtests, mocker) diff --git a/system/athena/tests/test_registration.py b/openpilot/system/athena/tests/test_registration.py similarity index 98% rename from system/athena/tests/test_registration.py rename to openpilot/system/athena/tests/test_registration.py index f5d900d4db..bb1523de80 100644 --- a/system/athena/tests/test_registration.py +++ b/openpilot/system/athena/tests/test_registration.py @@ -5,7 +5,7 @@ from pathlib import Path from openpilot.common.params import Params from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_ID from openpilot.system.athena.tests.helpers import MockResponse -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths class TestRegistration: diff --git a/system/camerad/SConscript b/openpilot/system/camerad/SConscript similarity index 100% rename from system/camerad/SConscript rename to openpilot/system/camerad/SConscript diff --git a/system/athena/tests/__init__.py b/openpilot/system/camerad/__init__.py similarity index 100% rename from system/athena/tests/__init__.py rename to openpilot/system/camerad/__init__.py diff --git a/system/camerad/cameras/bps_blobs.h b/openpilot/system/camerad/cameras/bps_blobs.h similarity index 100% rename from system/camerad/cameras/bps_blobs.h rename to openpilot/system/camerad/cameras/bps_blobs.h diff --git a/system/camerad/cameras/camera_common.cc b/openpilot/system/camerad/cameras/camera_common.cc similarity index 100% rename from system/camerad/cameras/camera_common.cc rename to openpilot/system/camerad/cameras/camera_common.cc diff --git a/system/camerad/cameras/camera_common.h b/openpilot/system/camerad/cameras/camera_common.h similarity index 95% rename from system/camerad/cameras/camera_common.h rename to openpilot/system/camerad/cameras/camera_common.h index 7f35e06a83..24f70dbcf3 100644 --- a/system/camerad/cameras/camera_common.h +++ b/openpilot/system/camerad/cameras/camera_common.h @@ -2,7 +2,7 @@ #include -#include "cereal/messaging/messaging.h" +#include "openpilot/cereal/messaging/messaging.h" #include "msgq/visionipc/visionipc_server.h" #include "common/util.h" diff --git a/system/camerad/cameras/camera_qcom2.cc b/openpilot/system/camerad/cameras/camera_qcom2.cc similarity index 100% rename from system/camerad/cameras/camera_qcom2.cc rename to openpilot/system/camerad/cameras/camera_qcom2.cc diff --git a/system/camerad/cameras/cdm.cc b/openpilot/system/camerad/cameras/cdm.cc similarity index 100% rename from system/camerad/cameras/cdm.cc rename to openpilot/system/camerad/cameras/cdm.cc diff --git a/system/camerad/cameras/cdm.h b/openpilot/system/camerad/cameras/cdm.h similarity index 100% rename from system/camerad/cameras/cdm.h rename to openpilot/system/camerad/cameras/cdm.h diff --git a/system/camerad/cameras/hw.h b/openpilot/system/camerad/cameras/hw.h similarity index 97% rename from system/camerad/cameras/hw.h rename to openpilot/system/camerad/cameras/hw.h index defe878e89..be0bea872d 100644 --- a/system/camerad/cameras/hw.h +++ b/openpilot/system/camerad/cameras/hw.h @@ -1,7 +1,7 @@ #pragma once #include "common/util.h" -#include "cereal/gen/cpp/log.capnp.h" +#include "openpilot/cereal/gen/cpp/log.capnp.h" #include "msgq/visionipc/visionipc_server.h" #include "media/cam_isp_ife.h" diff --git a/system/camerad/cameras/ife.h b/openpilot/system/camerad/cameras/ife.h similarity index 100% rename from system/camerad/cameras/ife.h rename to openpilot/system/camerad/cameras/ife.h diff --git a/system/camerad/cameras/nv12_info.h b/openpilot/system/camerad/cameras/nv12_info.h similarity index 100% rename from system/camerad/cameras/nv12_info.h rename to openpilot/system/camerad/cameras/nv12_info.h diff --git a/system/camerad/cameras/nv12_info.py b/openpilot/system/camerad/cameras/nv12_info.py similarity index 92% rename from system/camerad/cameras/nv12_info.py rename to openpilot/system/camerad/cameras/nv12_info.py index 2cadc29364..abd734ba50 100644 --- a/system/camerad/cameras/nv12_info.py +++ b/openpilot/system/camerad/cameras/nv12_info.py @@ -1,4 +1,4 @@ -# Python version of system/camerad/cameras/nv12_info.h +# Python version of openpilot/system/camerad/cameras/nv12_info.h # Calculations from media/msm_media_info.h (VENUS_BUFFER_SIZE) def align(val: int, alignment: int) -> int: diff --git a/system/camerad/cameras/spectra.cc b/openpilot/system/camerad/cameras/spectra.cc similarity index 100% rename from system/camerad/cameras/spectra.cc rename to openpilot/system/camerad/cameras/spectra.cc diff --git a/system/camerad/cameras/spectra.h b/openpilot/system/camerad/cameras/spectra.h similarity index 100% rename from system/camerad/cameras/spectra.h rename to openpilot/system/camerad/cameras/spectra.h diff --git a/system/camerad/main.cc b/openpilot/system/camerad/main.cc similarity index 100% rename from system/camerad/main.cc rename to openpilot/system/camerad/main.cc diff --git a/system/camerad/sensors/os04c10.cc b/openpilot/system/camerad/sensors/os04c10.cc similarity index 100% rename from system/camerad/sensors/os04c10.cc rename to openpilot/system/camerad/sensors/os04c10.cc diff --git a/system/camerad/sensors/os04c10_registers.h b/openpilot/system/camerad/sensors/os04c10_registers.h similarity index 100% rename from system/camerad/sensors/os04c10_registers.h rename to openpilot/system/camerad/sensors/os04c10_registers.h diff --git a/system/camerad/sensors/ox03c10.cc b/openpilot/system/camerad/sensors/ox03c10.cc similarity index 100% rename from system/camerad/sensors/ox03c10.cc rename to openpilot/system/camerad/sensors/ox03c10.cc diff --git a/system/camerad/sensors/ox03c10_registers.h b/openpilot/system/camerad/sensors/ox03c10_registers.h similarity index 100% rename from system/camerad/sensors/ox03c10_registers.h rename to openpilot/system/camerad/sensors/ox03c10_registers.h diff --git a/system/camerad/sensors/sensor.h b/openpilot/system/camerad/sensors/sensor.h similarity index 98% rename from system/camerad/sensors/sensor.h rename to openpilot/system/camerad/sensors/sensor.h index 1a647d1528..251f5d64b4 100644 --- a/system/camerad/sensors/sensor.h +++ b/openpilot/system/camerad/sensors/sensor.h @@ -9,7 +9,7 @@ #include "media/cam_isp.h" #include "media/cam_sensor.h" -#include "cereal/gen/cpp/log.capnp.h" +#include "openpilot/cereal/gen/cpp/log.capnp.h" #include "system/camerad/sensors/ox03c10_registers.h" #include "system/camerad/sensors/os04c10_registers.h" diff --git a/system/camerad/snapshot.py b/openpilot/system/camerad/snapshot.py similarity index 53% rename from system/camerad/snapshot.py rename to openpilot/system/camerad/snapshot.py index ec6cbc8ad3..8383865fce 100755 --- a/system/camerad/snapshot.py +++ b/openpilot/system/camerad/snapshot.py @@ -1,17 +1,10 @@ #!/usr/bin/env python3 -import subprocess -import time import numpy as np -from PIL import Image -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from msgq.visionipc import VisionIpcClient, VisionStreamType -from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL -from openpilot.system.hardware import PC -from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert -from openpilot.system.manager.process_config import managed_processes VISION_STREAMS = { @@ -21,11 +14,6 @@ VISION_STREAMS = { } -def jpeg_write(fn, dat): - img = Image.fromarray(dat) - img.save(fn, "JPEG") - - def yuv_to_rgb(y, u, v): ul = np.repeat(np.repeat(u, 2).reshape(u.shape[0], y.shape[1]), 2, axis=0).reshape(y.shape) vl = np.repeat(np.repeat(v, 2).reshape(v.shape[0], y.shape[1]), 2, axis=0).reshape(y.shape) @@ -77,55 +65,3 @@ def get_snapshots(frame="roadCameraState", front_frame="driverCameraState"): c = vipc_clients[front_frame] front = extract_image(c.recv()) return rear, front - - -def snapshot(): - params = Params() - - if (not params.get_bool("IsOffroad")) or params.get_bool("IsTakingSnapshot"): - print("Already taking snapshot") - return None, None - - front_camera_allowed = params.get_bool("RecordFront") - params.put_bool("IsTakingSnapshot", True, block=True) - set_offroad_alert("Offroad_IsTakingSnapshot", True) - time.sleep(2.0) # Give hardwared time to read the param, or if just started give camerad time to start - - # Check if camerad is already started - try: - subprocess.check_call(["pgrep", "camerad"]) - print("Camerad already running") - params.put_bool("IsTakingSnapshot", False, block=True) - params.remove("Offroad_IsTakingSnapshot") - return None, None - except subprocess.CalledProcessError: - pass - - try: - # Allow testing on replay on PC - if not PC: - managed_processes['camerad'].start() - - frame = "wideRoadCameraState" - front_frame = "driverCameraState" if front_camera_allowed else None - rear, front = get_snapshots(frame, front_frame) - finally: - managed_processes['camerad'].stop() - params.put_bool("IsTakingSnapshot", False, block=True) - set_offroad_alert("Offroad_IsTakingSnapshot", False) - - if not front_camera_allowed: - front = None - - return rear, front - - -if __name__ == "__main__": - pic, fpic = snapshot() - if pic is not None: - print(pic.shape) - jpeg_write("/tmp/back.jpg", pic) - if fpic is not None: - jpeg_write("/tmp/front.jpg", fpic) - else: - print("Error taking snapshot") diff --git a/system/camerad/test/.gitignore b/openpilot/system/camerad/test/.gitignore similarity index 100% rename from system/camerad/test/.gitignore rename to openpilot/system/camerad/test/.gitignore diff --git a/system/camerad/test/debug.sh b/openpilot/system/camerad/test/debug.sh similarity index 100% rename from system/camerad/test/debug.sh rename to openpilot/system/camerad/test/debug.sh diff --git a/system/camerad/test/icp_debug.sh b/openpilot/system/camerad/test/icp_debug.sh similarity index 100% rename from system/camerad/test/icp_debug.sh rename to openpilot/system/camerad/test/icp_debug.sh diff --git a/system/camerad/test/intercept.sh b/openpilot/system/camerad/test/intercept.sh similarity index 100% rename from system/camerad/test/intercept.sh rename to openpilot/system/camerad/test/intercept.sh diff --git a/system/camerad/test/stress_restart.sh b/openpilot/system/camerad/test/stress_restart.sh similarity index 100% rename from system/camerad/test/stress_restart.sh rename to openpilot/system/camerad/test/stress_restart.sh diff --git a/system/camerad/test/test_ae_gray.cc b/openpilot/system/camerad/test/test_ae_gray.cc similarity index 100% rename from system/camerad/test/test_ae_gray.cc rename to openpilot/system/camerad/test/test_ae_gray.cc diff --git a/system/camerad/test/test_camerad.py b/openpilot/system/camerad/test/test_camerad.py similarity index 99% rename from system/camerad/test/test_camerad.py rename to openpilot/system/camerad/test/test_camerad.py index 3abe4db654..afc49c02bf 100644 --- a/system/camerad/test/test_camerad.py +++ b/openpilot/system/camerad/test/test_camerad.py @@ -3,7 +3,7 @@ import time import pytest import numpy as np -from cereal.services import SERVICE_LIST +from openpilot.cereal.services import SERVICE_LIST from openpilot.tools.lib.log_time_series import msgs_to_time_series from openpilot.system.camerad.snapshot import get_snapshots from openpilot.selfdrive.test.helpers import collect_logs, log_collector, processes_context diff --git a/tools/webcam/README.md b/openpilot/system/camerad/webcam/README.md similarity index 55% rename from tools/webcam/README.md rename to openpilot/system/camerad/webcam/README.md index 6abbc47935..17d7ee6d80 100644 --- a/tools/webcam/README.md +++ b/openpilot/system/camerad/webcam/README.md @@ -1,13 +1,5 @@ # Run openpilot with webcam on PC -What's needed: -- Ubuntu 24.04 ([WSL2 is not supported](https://github.com/commaai/openpilot/issues/34216)) or macOS -- GPU (recommended) -- One USB webcam, at least 720p and 78 degrees FOV (e.g. Logitech C920/C615, NexiGo N60) -- [Car harness](https://comma.ai/shop/products/comma-car-harness) -- [panda](https://comma.ai/shop/panda) -- USB-A to USB-A cable to connect panda to your computer - ## Setup openpilot - Follow [this readme](../README.md) to install and build the requirements @@ -16,8 +8,9 @@ What's needed: - Connect your computer to panda ## GO + ``` -USE_WEBCAM=1 system/manager/manager.py +USE_WEBCAM=1 openpilot/system/manager/manager.py ``` - Start the car, then the UI should show the road webcam's view - Adjust and secure the webcam @@ -27,5 +20,5 @@ USE_WEBCAM=1 system/manager/manager.py Use the `ROAD_CAM` (default 0) and optional `DRIVER_CAM`, `WIDE_CAM` environment variables to specify which camera is which (ie. `ROAD_CAM=1` uses `/dev/video1`, on Ubuntu, for the road camera): ``` -USE_WEBCAM=1 ROAD_CAM=1 system/manager/manager.py +USE_WEBCAM=1 ROAD_CAM=1 openpilot/system/manager/manager.py ``` diff --git a/tools/webcam/camera.py b/openpilot/system/camerad/webcam/camera.py similarity index 100% rename from tools/webcam/camera.py rename to openpilot/system/camerad/webcam/camera.py diff --git a/tools/webcam/camerad.py b/openpilot/system/camerad/webcam/camerad.py similarity index 95% rename from tools/webcam/camerad.py rename to openpilot/system/camerad/webcam/camerad.py index f1e70d948a..c46482360d 100755 --- a/tools/webcam/camerad.py +++ b/openpilot/system/camerad/webcam/camerad.py @@ -5,9 +5,9 @@ import platform from collections import namedtuple from msgq.visionipc import VisionIpcServer, VisionStreamType -from cereal import messaging +from openpilot.cereal import messaging -from openpilot.tools.webcam.camera import Camera +from openpilot.system.camerad.webcam.camera import Camera from openpilot.common.realtime import Ratekeeper ROAD_CAM = os.getenv("ROAD_CAM", "0") diff --git a/system/hardware/fan_controller.py b/openpilot/system/hardware/fan_controller.py similarity index 95% rename from system/hardware/fan_controller.py rename to openpilot/system/hardware/fan_controller.py index eafde599ad..869fe19c3d 100755 --- a/system/hardware/fan_controller.py +++ b/openpilot/system/hardware/fan_controller.py @@ -2,7 +2,7 @@ import numpy as np from openpilot.common.pid import PIDController -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE # raise fan setpoint on tici/tizi to reduce noise # after raising LMH threshold in AGNOS 18.1 to prevent CPU throttling diff --git a/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py similarity index 95% rename from system/hardware/hardwared.py rename to openpilot/system/hardware/hardwared.py index 6c84ccfe8a..e5b3d8020c 100755 --- a/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -7,23 +7,24 @@ import threading import time from collections import OrderedDict, namedtuple -import psutil - -import cereal.messaging as messaging -from cereal import log -from cereal.services import SERVICE_LIST +import openpilot.cereal.messaging as messaging +from openpilot.cereal import log +from openpilot.cereal.services import SERVICE_LIST from openpilot.common.utils import strip_deprecated_keys from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_HW from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert -from openpilot.system.hardware import HARDWARE, TICI, AGNOS +from openpilot.common.hardware import HARDWARE, TICI +from openpilot.common.hardware.usb import get_usb_state, set_usb_state +from openpilot.common.linux import LinuxSystemStats from openpilot.system.loggerd.config import get_available_percent -from openpilot.system.statsd import statlog from openpilot.common.swaglog import cloudlog +from openpilot.sunnypilot.system.statsd import statlog from openpilot.system.hardware.power_monitoring import PowerMonitoring from openpilot.system.hardware.fan_controller import FanController -from openpilot.system.version import terms_version, training_version, get_build_metadata, terms_version_sp +from openpilot.common.version import terms_version, training_version, get_build_metadata, terms_version_sp + ThermalStatus = log.DeviceState.ThermalStatus NetworkType = log.DeviceState.NetworkType @@ -36,7 +37,7 @@ ONROAD_CYCLE_TIME = 1 # seconds to wait offroad after requesting an onroad cycl ThermalBand = namedtuple("ThermalBand", ['min_temp', 'max_temp']) HardwareState = namedtuple("HardwareState", ['network_type', 'network_info', 'network_strength', 'network_stats', - 'network_metered', 'modem_temps']) + 'network_metered', 'modem_temps', 'usb_state']) # List of thermal bands. We will stay within this region as long as we are within the bounds. # When exiting the bounds, we'll jump to the lower or higher band. Bands are ordered in the dict. @@ -106,8 +107,6 @@ def hw_state_thread(end_event, hw_queue): count = 0 prev_hw_state = None - modem_version = None - while not end_event.is_set(): # these are expensive calls. update every 10s if (count % int(10. / DT_HW)) == 0: @@ -117,13 +116,6 @@ def hw_state_thread(end_event, hw_queue): if len(modem_temps) == 0 and prev_hw_state is not None: modem_temps = prev_hw_state.modem_temps - # Log modem version once - if AGNOS and (modem_version is None): - modem_version = HARDWARE.get_modem_version() - - if modem_version is not None: - cloudlog.event("modem version", version=modem_version) - tx, rx = HARDWARE.get_modem_data_usage() hw_state = HardwareState( @@ -133,6 +125,7 @@ def hw_state_thread(end_event, hw_queue): network_stats={'wwanTx': tx, 'wwanRx': rx}, network_metered=HARDWARE.get_network_metered(network_type), modem_temps=modem_temps, + usb_state=get_usb_state(), ) try: @@ -149,6 +142,7 @@ def hw_state_thread(end_event, hw_queue): def hardware_thread(end_event, hw_queue) -> None: + system_stats = LinuxSystemStats() pm = messaging.PubMaster(['deviceState']) sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates"], poll="pandaStates") @@ -175,6 +169,7 @@ def hardware_thread(end_event, hw_queue) -> None: network_strength=NetworkStrength.unknown, network_stats={'wwanTx': -1, 'wwanRx': -1}, modem_temps=[], + usb_state=[], ) all_temp_filter = FirstOrderFilter(0., TEMP_TAU, DT_HW, initialized=False) @@ -238,9 +233,9 @@ def hardware_thread(end_event, hw_queue) -> None: pass msg.deviceState.freeSpacePercent = get_available_percent(default=100.0) - msg.deviceState.memoryUsagePercent = int(round(psutil.virtual_memory().percent)) + msg.deviceState.memoryUsagePercent = int(round(system_stats.memory_usage_percent())) msg.deviceState.gpuUsagePercent = int(round(HARDWARE.get_gpu_usage_percent())) - online_cpu_usage = [int(round(n)) for n in psutil.cpu_percent(percpu=True)] + online_cpu_usage = [int(round(n)) for n in system_stats.cpu_usage_percent()] offline_cpu_usage = [0., ] * (len(msg.deviceState.cpuTempC) - len(online_cpu_usage)) msg.deviceState.cpuUsagePercent = online_cpu_usage + offline_cpu_usage @@ -255,6 +250,8 @@ def hardware_thread(end_event, hw_queue) -> None: msg.deviceState.screenBrightnessPercent = HARDWARE.get_screen_brightness() + set_usb_state(msg.deviceState, last_hw_state.usb_state) + # this subset is only used for offroad temp_sources = [ msg.deviceState.memoryTempC, @@ -295,7 +292,6 @@ def hardware_thread(end_event, hw_queue) -> None: startup_conditions["free_space"] = msg.deviceState.freeSpacePercent > 2 startup_conditions["completed_training"] = params.get("CompletedTrainingVersion") == training_version startup_conditions["not_driver_view"] = not params.get_bool("IsDriverViewEnabled") - startup_conditions["not_taking_snapshot"] = not params.get_bool("IsTakingSnapshot") # must be at an engageable thermal band to go onroad startup_conditions["device_temp_engageable"] = thermal_status < ThermalStatus.overheated @@ -406,7 +402,6 @@ def hardware_thread(end_event, hw_queue) -> None: msg.deviceState.thermalStatus = thermal_status pm.send("deviceState", msg) - # Log to statsd statlog.gauge("free_space_percent", msg.deviceState.freeSpacePercent) statlog.gauge("gpu_usage_percent", msg.deviceState.gpuUsagePercent) statlog.gauge("memory_usage_percent", msg.deviceState.memoryUsagePercent) diff --git a/system/hardware/power_monitoring.py b/openpilot/system/hardware/power_monitoring.py similarity index 97% rename from system/hardware/power_monitoring.py rename to openpilot/system/hardware/power_monitoring.py index 50a83682cf..bd9c334d04 100644 --- a/system/hardware/power_monitoring.py +++ b/openpilot/system/hardware/power_monitoring.py @@ -2,9 +2,9 @@ import time import threading from openpilot.common.params import Params -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.common.swaglog import cloudlog -from openpilot.system.statsd import statlog +from openpilot.sunnypilot.system.statsd import statlog CAR_VOLTAGE_LOW_PASS_K = 0.011 # LPF gain for 45s tau (dt/tau / (dt/tau + 1)) @@ -35,7 +35,7 @@ class PowerMonitoring: self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), car_battery_capacity_uWh) # Calculation tick - def calculate(self, voltage: int | None, ignition: bool): + def calculate(self, voltage: float | None, ignition: bool): try: now = time.monotonic() diff --git a/system/camerad/__init__.py b/openpilot/system/hardware/tests/__init__.py similarity index 100% rename from system/camerad/__init__.py rename to openpilot/system/hardware/tests/__init__.py diff --git a/system/hardware/tests/test_fan_controller.py b/openpilot/system/hardware/tests/test_fan_controller.py similarity index 61% rename from system/hardware/tests/test_fan_controller.py rename to openpilot/system/hardware/tests/test_fan_controller.py index ee39a24f87..e1aceeb081 100644 --- a/system/hardware/tests/test_fan_controller.py +++ b/openpilot/system/hardware/tests/test_fan_controller.py @@ -4,10 +4,6 @@ from openpilot.system.hardware.fan_controller import FanController ALL_CONTROLLERS = [FanController] -def patched_controller(mocker, controller_class): - mocker.patch("os.system", new=mocker.Mock()) - return controller_class(2) - class TestFanController: def wind_up(self, controller, ignition=True): for _ in range(1000): @@ -18,32 +14,32 @@ class TestFanController: controller.update(10, ignition) @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) - def test_hot_onroad(self, mocker, controller_class): - controller = patched_controller(mocker, controller_class) + def test_hot_onroad(self, controller_class): + controller = controller_class(2) self.wind_up(controller) assert controller.update(100, True) >= 70 @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) - def test_offroad_limits(self, mocker, controller_class): - controller = patched_controller(mocker, controller_class) + def test_offroad_limits(self, controller_class): + controller = controller_class(2) self.wind_up(controller) assert controller.update(100, False) <= 30 @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) - def test_no_fan_wear(self, mocker, controller_class): - controller = patched_controller(mocker, controller_class) + def test_no_fan_wear(self, controller_class): + controller = controller_class(2) self.wind_down(controller) assert controller.update(10, False) == 0 @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) - def test_limited(self, mocker, controller_class): - controller = patched_controller(mocker, controller_class) + def test_limited(self, controller_class): + controller = controller_class(2) self.wind_up(controller, True) assert controller.update(100, True) == 100 @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) - def test_windup_speed(self, mocker, controller_class): - controller = patched_controller(mocker, controller_class) + def test_windup_speed(self, controller_class): + controller = controller_class(2) self.wind_down(controller, True) for _ in range(10): controller.update(90, True) diff --git a/system/hardware/tests/test_power_monitoring.py b/openpilot/system/hardware/tests/test_power_monitoring.py similarity index 99% rename from system/hardware/tests/test_power_monitoring.py rename to openpilot/system/hardware/tests/test_power_monitoring.py index f0804c2071..12057ab837 100644 --- a/system/hardware/tests/test_power_monitoring.py +++ b/openpilot/system/hardware/tests/test_power_monitoring.py @@ -35,7 +35,7 @@ class TestPowerMonitoring: def test_panda_state_present(self): pm = PowerMonitoring() for _ in range(10): - pm.calculate(None, None) + pm.calculate(None, False) assert pm.get_power_used() == 0 assert pm.get_car_battery_capacity() == (CAR_BATTERY_CAPACITY_uWh / 10) diff --git a/openpilot/system/hardware/tici/agnos.json b/openpilot/system/hardware/tici/agnos.json new file mode 120000 index 0000000000..ffee992f25 --- /dev/null +++ b/openpilot/system/hardware/tici/agnos.json @@ -0,0 +1 @@ +../../../common/hardware/tici/agnos.json \ No newline at end of file diff --git a/system/journald.py b/openpilot/system/journald.py similarity index 79% rename from system/journald.py rename to openpilot/system/journald.py index 37158b9251..091494b652 100755 --- a/system/journald.py +++ b/openpilot/system/journald.py @@ -2,12 +2,12 @@ import json import subprocess -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.common.swaglog import cloudlog def main(): - pm = messaging.PubMaster(['androidLog']) + pm = messaging.PubMaster(['operatingSystemLog']) cmd = ['journalctl', '-f', '-o', 'json'] proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True) assert proc.stdout is not None @@ -22,8 +22,8 @@ def main(): cloudlog.exception("failed to parse journalctl output") continue - msg = messaging.new_message('androidLog') - entry = msg.androidLog + msg = messaging.new_message('operatingSystemLog') + entry = msg.operatingSystemLog entry.ts = int(kv.get('__REALTIME_TIMESTAMP', 0)) entry.message = json.dumps(kv) if '_PID' in kv: @@ -33,7 +33,7 @@ def main(): if 'SYSLOG_IDENTIFIER' in kv: entry.tag = kv['SYSLOG_IDENTIFIER'] - pm.send('androidLog', msg) + pm.send('operatingSystemLog', msg) finally: proc.terminate() proc.wait() diff --git a/system/loggerd/.gitignore b/openpilot/system/loggerd/.gitignore similarity index 100% rename from system/loggerd/.gitignore rename to openpilot/system/loggerd/.gitignore diff --git a/system/loggerd/SConscript b/openpilot/system/loggerd/SConscript similarity index 65% rename from system/loggerd/SConscript rename to openpilot/system/loggerd/SConscript index a638704dca..7f6d4faf0c 100644 --- a/system/loggerd/SConscript +++ b/openpilot/system/loggerd/SConscript @@ -1,8 +1,6 @@ -Import('env', 'arch', 'messaging', 'common', 'visionipc') +Import('env', 'arch', 'messaging', 'common', 'visionipc', 'ffmpeg_libs') -libs = [common, messaging, visionipc, - 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', - 'pthread', 'z', 'm', 'zstd'] +libs = [common, messaging, visionipc] + ffmpeg_libs + ['pthread', 'm', 'zstd'] frameworks = [] src = ['logger.cc', 'zstd_writer.cc', 'video_writer.cc', 'encoder/encoder.cc', 'encoder/jpeg_encoder.cc'] @@ -10,18 +8,14 @@ if arch == "larch64": src += ['encoder/v4l_encoder.cc'] else: src += ['encoder/ffmpeg_encoder.cc'] - libs += ['yuv'] if arch == "Darwin": frameworks += ['VideoToolbox', 'CoreMedia', 'CoreFoundation', 'CoreVideo'] -if arch != "Darwin": - libs += ['va', 'va-drm', 'drm'] - logger_lib = env.Library('logger', src) libs.insert(0, logger_lib) env.Program('loggerd', ['loggerd.cc'], LIBS=libs, FRAMEWORKS=frameworks) -env.Program('encoderd', ['encoderd.cc'], LIBS=libs + ["jpeg"], FRAMEWORKS=frameworks) +env.Program('encoderd', ['encoderd.cc'], LIBS=libs, FRAMEWORKS=frameworks) env.Program('bootlog.cc', LIBS=libs, FRAMEWORKS=frameworks) if GetOption('extras'): diff --git a/system/hardware/pc/__init__.py b/openpilot/system/loggerd/__init__.py similarity index 100% rename from system/hardware/pc/__init__.py rename to openpilot/system/loggerd/__init__.py diff --git a/system/loggerd/bootlog.cc b/openpilot/system/loggerd/bootlog.cc similarity index 92% rename from system/loggerd/bootlog.cc rename to openpilot/system/loggerd/bootlog.cc index 1b87ce394f..b2e3abfcf1 100644 --- a/system/loggerd/bootlog.cc +++ b/openpilot/system/loggerd/bootlog.cc @@ -1,7 +1,7 @@ #include #include -#include "cereal/messaging/messaging.h" +#include "openpilot/cereal/messaging/messaging.h" #include "common/params.h" #include "common/swaglog.h" #include "system/loggerd/logger.h" @@ -28,7 +28,7 @@ static kj::Array build_boot_log() { // Gather output of commands std::vector bootlog_commands = { - "[ -x \"$(command -v journalctl)\" ] && journalctl -o short-monotonic", + "[ -x \"$(command -v journalctl)\" ] && journalctl -b -n 2000 -o short-monotonic --no-pager", }; diff --git a/system/loggerd/config.py b/openpilot/system/loggerd/config.py similarity index 94% rename from system/loggerd/config.py rename to openpilot/system/loggerd/config.py index d9befb5613..e6bd97b99d 100644 --- a/system/loggerd/config.py +++ b/openpilot/system/loggerd/config.py @@ -1,5 +1,5 @@ import os -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths CAMERA_FPS = 20 diff --git a/system/loggerd/deleter.py b/openpilot/system/loggerd/deleter.py similarity index 98% rename from system/loggerd/deleter.py rename to openpilot/system/loggerd/deleter.py index 058f5c301d..d5c12474f8 100755 --- a/system/loggerd/deleter.py +++ b/openpilot/system/loggerd/deleter.py @@ -4,7 +4,7 @@ import time import shutil import threading from pathlib import Path -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog from openpilot.system.loggerd.config import get_available_bytes, get_available_percent from openpilot.system.loggerd.uploader import listdir_by_creation diff --git a/system/loggerd/encoder/encoder.cc b/openpilot/system/loggerd/encoder/encoder.cc similarity index 100% rename from system/loggerd/encoder/encoder.cc rename to openpilot/system/loggerd/encoder/encoder.cc diff --git a/system/loggerd/encoder/encoder.h b/openpilot/system/loggerd/encoder/encoder.h similarity index 92% rename from system/loggerd/encoder/encoder.h rename to openpilot/system/loggerd/encoder/encoder.h index 0a136f769c..d333351cb4 100644 --- a/system/loggerd/encoder/encoder.h +++ b/openpilot/system/loggerd/encoder/encoder.h @@ -14,7 +14,7 @@ #include #include -#include "cereal/messaging/messaging.h" +#include "openpilot/cereal/messaging/messaging.h" #include "msgq/visionipc/visionipc.h" #include "common/queue.h" #include "system/loggerd/loggerd.h" @@ -27,6 +27,7 @@ public: virtual void encoder_open() = 0; virtual void encoder_close() = 0; virtual void set_bitrate(int bitrate) = 0; + virtual void request_keyframe() = 0; void publisher_publish(int segment_num, uint32_t idx, VisionIpcBufExtra &extra, unsigned int flags, kj::ArrayPtr header, kj::ArrayPtr dat); diff --git a/system/loggerd/encoder/ffmpeg_encoder.cc b/openpilot/system/loggerd/encoder/ffmpeg_encoder.cc similarity index 84% rename from system/loggerd/encoder/ffmpeg_encoder.cc rename to openpilot/system/loggerd/encoder/ffmpeg_encoder.cc index f6399659b4..451445e5a0 100644 --- a/system/loggerd/encoder/ffmpeg_encoder.cc +++ b/openpilot/system/loggerd/encoder/ffmpeg_encoder.cc @@ -9,8 +9,6 @@ #define __STDC_CONSTANT_MACROS -#include "libyuv.h" - extern "C" { #include #include @@ -19,6 +17,7 @@ extern "C" { #include "common/swaglog.h" #include "common/util.h" +#include "common/yuv.h" const int env_debug_encoder = (getenv("DEBUG_ENCODER") != NULL) ? atoi(getenv("DEBUG_ENCODER")) : 0; @@ -76,6 +75,10 @@ void FfmpegEncoder::set_bitrate(int bitrate) { LOGE("adaptive bitrate is not supported for ffmpeg encoder %s", encoder_info.publish_name); } +void FfmpegEncoder::request_keyframe() { + LOGE("keyframe request is not supported for ffmpeg encoder %s", encoder_info.publish_name); +} + int FfmpegEncoder::encode_frame(VisionBuf* buf, VisionIpcBufExtra *extra) { assert(buf->width == this->in_width); assert(buf->height == this->in_height); @@ -83,26 +86,25 @@ int FfmpegEncoder::encode_frame(VisionBuf* buf, VisionIpcBufExtra *extra) { uint8_t *cy = convert_buf.data(); uint8_t *cu = cy + in_width * in_height; uint8_t *cv = cu + (in_width / 2) * (in_height / 2); - libyuv::NV12ToI420(buf->y, buf->stride, - buf->uv, buf->stride, - cy, in_width, - cu, in_width/2, - cv, in_width/2, - in_width, in_height); + yuv::nv12_to_i420(buf->y, buf->stride, + buf->uv, buf->stride, + cy, in_width, + cu, in_width/2, + cv, in_width/2, + in_width, in_height); if (downscale_buf.size() > 0) { uint8_t *out_y = downscale_buf.data(); uint8_t *out_u = out_y + frame->width * frame->height; uint8_t *out_v = out_u + (frame->width / 2) * (frame->height / 2); - libyuv::I420Scale(cy, in_width, - cu, in_width/2, - cv, in_width/2, - in_width, in_height, - out_y, frame->width, - out_u, frame->width/2, - out_v, frame->width/2, - frame->width, frame->height, - libyuv::kFilterNone); + yuv::i420_scale(cy, in_width, + cu, in_width/2, + cv, in_width/2, + in_width, in_height, + out_y, frame->width, + out_u, frame->width/2, + out_v, frame->width/2, + frame->width, frame->height); frame->data[0] = out_y; frame->data[1] = out_u; frame->data[2] = out_v; diff --git a/system/loggerd/encoder/ffmpeg_encoder.h b/openpilot/system/loggerd/encoder/ffmpeg_encoder.h similarity index 96% rename from system/loggerd/encoder/ffmpeg_encoder.h rename to openpilot/system/loggerd/encoder/ffmpeg_encoder.h index a85e1d3816..3f7c674db3 100644 --- a/system/loggerd/encoder/ffmpeg_encoder.h +++ b/openpilot/system/loggerd/encoder/ffmpeg_encoder.h @@ -22,6 +22,7 @@ public: void encoder_open(); void encoder_close(); void set_bitrate(int bitrate); + void request_keyframe(); private: int segment_num = -1; diff --git a/openpilot/system/loggerd/encoder/jpeg_encoder.cc b/openpilot/system/loggerd/encoder/jpeg_encoder.cc new file mode 100644 index 0000000000..6258e8144d --- /dev/null +++ b/openpilot/system/loggerd/encoder/jpeg_encoder.cc @@ -0,0 +1,115 @@ +#include "system/loggerd/encoder/jpeg_encoder.h" + +#include +#include + +#include "common/swaglog.h" + +// Lower qscale = higher quality / bigger files for MJPEG. +constexpr int MJPEG_QSCALE = 7; + +JpegEncoder::JpegEncoder(const std::string &publish_name, int width, int height) + : publish_name(publish_name), thumbnail_width(width), thumbnail_height(height) { + yuv_buffer.resize((thumbnail_width * thumbnail_height * 3) / 2); + pm = std::make_unique(std::vector{publish_name.c_str()}); + + const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG); + assert(codec); + + codec_ctx = avcodec_alloc_context3(codec); + assert(codec_ctx); + codec_ctx->width = thumbnail_width; + codec_ctx->height = thumbnail_height; + codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; + codec_ctx->time_base = (AVRational){1, 1}; + codec_ctx->color_range = AVCOL_RANGE_JPEG; + codec_ctx->flags |= AV_CODEC_FLAG_QSCALE; + codec_ctx->global_quality = FF_QP2LAMBDA * MJPEG_QSCALE; + + int err = avcodec_open2(codec_ctx, codec, NULL); + assert(err >= 0); + + frame = av_frame_alloc(); + assert(frame); + frame->format = codec_ctx->pix_fmt; + frame->width = thumbnail_width; + frame->height = thumbnail_height; + frame->linesize[0] = thumbnail_width; + frame->linesize[1] = thumbnail_width / 2; + frame->linesize[2] = thumbnail_width / 2; + frame->color_range = AVCOL_RANGE_JPEG; + + pkt = av_packet_alloc(); + assert(pkt); +} + +JpegEncoder::~JpegEncoder() { + av_packet_free(&pkt); + av_frame_free(&frame); + avcodec_free_context(&codec_ctx); +} + +void JpegEncoder::pushThumbnail(VisionBuf *buf, const VisionIpcBufExtra &extra) { + generateThumbnail(buf->y, buf->uv, buf->width, buf->height, buf->stride); + + MessageBuilder msg; + auto thumbnaild = msg.initEvent().initThumbnail(); + thumbnaild.setFrameId(extra.frame_id); + thumbnaild.setTimestampEof(extra.timestamp_eof); + thumbnaild.setThumbnail({out_buffer.data(), out_buffer.size()}); + + pm->send(publish_name.c_str(), msg); +} + +void JpegEncoder::generateThumbnail(const uint8_t *y_addr, const uint8_t *uv_addr, int width, int height, int stride) { + int downscale = width / thumbnail_width; + assert(downscale * thumbnail_height == height); + + uint8_t *y_plane = yuv_buffer.data(); + uint8_t *u_plane = y_plane + thumbnail_width * thumbnail_height; + uint8_t *v_plane = u_plane + (thumbnail_width * thumbnail_height) / 4; + { + // subsampled conversion from nv12 to yuv420p + for (int hy = 0; hy < thumbnail_height / 2; hy++) { + for (int hx = 0; hx < thumbnail_width / 2; hx++) { + int ix = hx * downscale + (downscale - 1) / 2; + int iy = hy * downscale + (downscale - 1) / 2; + y_plane[(hy * 2 + 0) * thumbnail_width + (hx * 2 + 0)] = y_addr[(iy * 2 + 0) * stride + ix * 2 + 0]; + y_plane[(hy * 2 + 0) * thumbnail_width + (hx * 2 + 1)] = y_addr[(iy * 2 + 0) * stride + ix * 2 + 1]; + y_plane[(hy * 2 + 1) * thumbnail_width + (hx * 2 + 0)] = y_addr[(iy * 2 + 1) * stride + ix * 2 + 0]; + y_plane[(hy * 2 + 1) * thumbnail_width + (hx * 2 + 1)] = y_addr[(iy * 2 + 1) * stride + ix * 2 + 1]; + u_plane[hy * thumbnail_width / 2 + hx] = uv_addr[iy * stride + ix * 2 + 0]; + v_plane[hy * thumbnail_width / 2 + hx] = uv_addr[iy * stride + ix * 2 + 1]; + } + } + } + + compressToJpeg(y_plane, u_plane, v_plane); +} + +void JpegEncoder::compressToJpeg(uint8_t *y_plane, uint8_t *u_plane, uint8_t *v_plane) { + frame->data[0] = y_plane; + frame->data[1] = u_plane; + frame->data[2] = v_plane; + // Required for MJPEG qscale to take effect (global_quality alone is not enough). + frame->quality = FF_QP2LAMBDA * MJPEG_QSCALE; + frame->pts = AV_NOPTS_VALUE; + + int err = avcodec_send_frame(codec_ctx, frame); + if (err < 0) { + LOGE("thumbnail avcodec_send_frame error %d", err); + out_buffer.clear(); + return; + } + + av_packet_unref(pkt); + err = avcodec_receive_packet(codec_ctx, pkt); + if (err < 0) { + LOGE("thumbnail avcodec_receive_packet error %d", err); + out_buffer.clear(); + return; + } + + out_buffer.assign(pkt->data, pkt->data + pkt->size); + av_packet_unref(pkt); +} diff --git a/system/loggerd/encoder/jpeg_encoder.h b/openpilot/system/loggerd/encoder/jpeg_encoder.h similarity index 63% rename from system/loggerd/encoder/jpeg_encoder.h rename to openpilot/system/loggerd/encoder/jpeg_encoder.h index af1427c19a..10cec52657 100644 --- a/system/loggerd/encoder/jpeg_encoder.h +++ b/openpilot/system/loggerd/encoder/jpeg_encoder.h @@ -1,18 +1,20 @@ #pragma once -#include -#include -#include #include -#include -#include #include -#include "cereal/messaging/messaging.h" +#include +#include + +#include "openpilot/cereal/messaging/messaging.h" #include "msgq/visionipc/visionbuf.h" +extern "C" { +#include +} + class JpegEncoder { public: - JpegEncoder(const std::string &pusblish_name, int width, int height); + JpegEncoder(const std::string &publish_name, int width, int height); ~JpegEncoder(); void pushThumbnail(VisionBuf *buf, const VisionIpcBufExtra &extra); @@ -24,9 +26,10 @@ private: int thumbnail_height; std::string publish_name; std::vector yuv_buffer; + std::vector out_buffer; std::unique_ptr pm; - // JPEG output buffer - unsigned char* out_buffer = nullptr; - unsigned long out_size = 0; + AVCodecContext *codec_ctx = nullptr; + AVFrame *frame = nullptr; + AVPacket *pkt = nullptr; }; diff --git a/system/loggerd/encoder/v4l_encoder.cc b/openpilot/system/loggerd/encoder/v4l_encoder.cc similarity index 97% rename from system/loggerd/encoder/v4l_encoder.cc rename to openpilot/system/loggerd/encoder/v4l_encoder.cc index 96a565f4ae..1b4d976b93 100644 --- a/system/loggerd/encoder/v4l_encoder.cc +++ b/openpilot/system/loggerd/encoder/v4l_encoder.cc @@ -156,7 +156,6 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei EncoderSettings encoder_settings = encoder_info.get_settings(in_width); current_bitrate = encoder_settings.bitrate; - adaptive_bitrate = encoder_info.adaptive_bitrate; bool is_h265 = encoder_settings.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C; struct v4l2_format fmt_out = { @@ -307,7 +306,7 @@ void V4LEncoder::encoder_close() { } void V4LEncoder::set_bitrate(int bitrate) { - if (!adaptive_bitrate || bitrate == current_bitrate) return; + if (bitrate == current_bitrate) return; if (bitrate <= 0) { LOGE("invalid livestream encoder bitrate %d", bitrate); return; @@ -325,6 +324,17 @@ void V4LEncoder::set_bitrate(int bitrate) { current_bitrate = bitrate; } +void V4LEncoder::request_keyframe() { + struct v4l2_control ctrl = { + .id = V4L2_CID_MPEG_VIDC_VIDEO_REQUEST_IFRAME, + .value = 1, + }; + + if (util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl) == -1) { + LOGE("failed to request keyframe for %s", encoder_info.publish_name); + } +} + V4LEncoder::~V4LEncoder() { encoder_close(); v4l2_buf_type buf_type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; diff --git a/system/loggerd/encoder/v4l_encoder.h b/openpilot/system/loggerd/encoder/v4l_encoder.h similarity index 96% rename from system/loggerd/encoder/v4l_encoder.h rename to openpilot/system/loggerd/encoder/v4l_encoder.h index 2985409605..b1a84e654f 100644 --- a/system/loggerd/encoder/v4l_encoder.h +++ b/openpilot/system/loggerd/encoder/v4l_encoder.h @@ -14,6 +14,7 @@ public: void encoder_open(); void encoder_close(); void set_bitrate(int bitrate); + void request_keyframe(); private: int fd; @@ -22,7 +23,6 @@ private: int segment_num = -1; int counter = 0; int current_bitrate = -1; - bool adaptive_bitrate; SafeQueue extras; diff --git a/system/loggerd/encoderd.cc b/openpilot/system/loggerd/encoderd.cc similarity index 92% rename from system/loggerd/encoderd.cc rename to openpilot/system/loggerd/encoderd.cc index 546990041a..11db07671d 100644 --- a/system/loggerd/encoderd.cc +++ b/openpilot/system/loggerd/encoderd.cc @@ -44,14 +44,18 @@ bool sync_encoders(EncoderdState *s, VisionStreamType cam_type, uint32_t frame_i } } -void apply_bitrate(std::vector> &encoders) { +void encoder_set_bitrate(std::unique_ptr &e) { static Params params; std::string val = params.get("LivestreamEncoderBitrate"); if (val.empty()) return; int bitrate = std::stoi(val); - for (auto &e : encoders) { - e->set_bitrate(bitrate); - } + e->set_bitrate(bitrate); +} + +void encoder_request_keyframe(std::unique_ptr &e) { + static Params params; + if (!params.getBool("LivestreamRequestKeyframe")) return; + e->request_keyframe(); } void encoder_thread(EncoderdState *s, const LogCameraInfo &cam_info) { @@ -59,9 +63,6 @@ void encoder_thread(EncoderdState *s, const LogCameraInfo &cam_info) { std::vector> encoders; - bool has_adaptive = std::any_of(cam_info.encoder_infos.begin(), cam_info.encoder_infos.end(), - [](const auto &ei) { return ei.adaptive_bitrate; }); - VisionIpcClient vipc_client = VisionIpcClient("camerad", cam_info.stream_type, false); std::unique_ptr jpeg_encoder; @@ -121,10 +122,13 @@ void encoder_thread(EncoderdState *s, const LogCameraInfo &cam_info) { ++cur_seg; } - if (has_adaptive) apply_bitrate(encoders); - // encode a frame for (int i = 0; i < encoders.size(); ++i) { + if (cam_info.encoder_infos[i].is_live) { + encoder_set_bitrate(encoders[i]); + encoder_request_keyframe(encoders[i]); + } + int out_id = encoders[i]->encode_frame(buf, &extra); if (out_id == -1) { diff --git a/system/loggerd/logger.cc b/openpilot/system/loggerd/logger.cc similarity index 100% rename from system/loggerd/logger.cc rename to openpilot/system/loggerd/logger.cc diff --git a/system/loggerd/logger.h b/openpilot/system/loggerd/logger.h similarity index 93% rename from system/loggerd/logger.h rename to openpilot/system/loggerd/logger.h index 18d07b5f38..419becfe5d 100644 --- a/system/loggerd/logger.h +++ b/openpilot/system/loggerd/logger.h @@ -4,9 +4,9 @@ #include #include -#include "cereal/messaging/messaging.h" +#include "openpilot/cereal/messaging/messaging.h" #include "common/util.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include "system/loggerd/zstd_writer.h" constexpr int LOG_COMPRESSION_LEVEL = 10; diff --git a/system/loggerd/loggerd.cc b/openpilot/system/loggerd/loggerd.cc similarity index 100% rename from system/loggerd/loggerd.cc rename to openpilot/system/loggerd/loggerd.cc diff --git a/system/loggerd/loggerd.h b/openpilot/system/loggerd/loggerd.h similarity index 96% rename from system/loggerd/loggerd.h rename to openpilot/system/loggerd/loggerd.h index 340e72e6fd..110dbe4fd2 100644 --- a/system/loggerd/loggerd.h +++ b/openpilot/system/loggerd/loggerd.h @@ -3,10 +3,10 @@ #include #include -#include "cereal/messaging/messaging.h" -#include "cereal/services.h" +#include "openpilot/cereal/messaging/messaging.h" +#include "openpilot/cereal/services.h" #include "msgq/visionipc/visionipc_client.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include "common/params.h" #include "common/swaglog.h" #include "common/util.h" @@ -59,7 +59,7 @@ public: const char *filename = NULL; bool record = true; bool include_audio = false; - bool adaptive_bitrate = false; + bool is_live = false; int frame_width = -1; int frame_height = -1; int fps = MAIN_FPS; @@ -105,7 +105,7 @@ const EncoderInfo stream_road_encoder_info = { .publish_name = "livestreamRoadEncodeData", //.thumbnail_name = "thumbnail", .record = false, - .adaptive_bitrate = true, + .is_live = true, .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamRoadEncode), }; @@ -113,7 +113,7 @@ const EncoderInfo stream_road_encoder_info = { const EncoderInfo stream_wide_road_encoder_info = { .publish_name = "livestreamWideRoadEncodeData", .record = false, - .adaptive_bitrate = true, + .is_live = true, .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamWideRoadEncode), }; @@ -121,7 +121,7 @@ const EncoderInfo stream_wide_road_encoder_info = { const EncoderInfo stream_driver_encoder_info = { .publish_name = "livestreamDriverEncodeData", .record = false, - .adaptive_bitrate = true, + .is_live = true, .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamDriverEncode), }; diff --git a/system/hardware/tests/__init__.py b/openpilot/system/loggerd/tests/__init__.py similarity index 100% rename from system/hardware/tests/__init__.py rename to openpilot/system/loggerd/tests/__init__.py diff --git a/system/loggerd/tests/loggerd_tests_common.py b/openpilot/system/loggerd/tests/loggerd_tests_common.py similarity index 92% rename from system/loggerd/tests/loggerd_tests_common.py rename to openpilot/system/loggerd/tests/loggerd_tests_common.py index fd071486df..55f5fbb816 100644 --- a/system/loggerd/tests/loggerd_tests_common.py +++ b/openpilot/system/loggerd/tests/loggerd_tests_common.py @@ -6,7 +6,7 @@ from pathlib import Path import openpilot.system.loggerd.deleter as deleter import openpilot.system.loggerd.uploader as uploader from openpilot.common.params import Params -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.system.loggerd.xattr_cache import setxattr @@ -63,10 +63,10 @@ class UploaderTestCase: seg_dir: str def set_ignore(self): - uploader.Api = MockApiIgnore + uploader.Api = MockApiIgnore # ty: ignore[invalid-assignment] # test double def setup_method(self): - uploader.Api = MockApi + uploader.Api = MockApi # ty: ignore[invalid-assignment] # test double uploader.fake_upload = True uploader.force_wifi = True uploader.allow_sleep = False diff --git a/system/loggerd/tests/test_deleter.py b/openpilot/system/loggerd/tests/test_deleter.py similarity index 97% rename from system/loggerd/tests/test_deleter.py rename to openpilot/system/loggerd/tests/test_deleter.py index 6222ea253b..8f18c8ceee 100644 --- a/system/loggerd/tests/test_deleter.py +++ b/openpilot/system/loggerd/tests/test_deleter.py @@ -19,7 +19,7 @@ class TestDeleter(UploaderTestCase): self.f_type = "fcamera.hevc" super().setup_method() self.fake_stats = Stats(f_bavail=0, f_blocks=10, f_frsize=4096) - deleter.os.statvfs = self.fake_statvfs + deleter.os.statvfs = self.fake_statvfs # ty: ignore[invalid-assignment] # test double def start_thread(self): self.end_event = threading.Event() diff --git a/system/loggerd/tests/test_encoder.py b/openpilot/system/loggerd/tests/test_encoder.py similarity index 91% rename from system/loggerd/tests/test_encoder.py rename to openpilot/system/loggerd/tests/test_encoder.py index 2b74fe2276..05bc211cbe 100644 --- a/system/loggerd/tests/test_encoder.py +++ b/openpilot/system/loggerd/tests/test_encoder.py @@ -1,21 +1,19 @@ import math import os import pytest -import random import shutil import subprocess import time from pathlib import Path -from openpilot.common.parameterized import parameterized from tqdm import trange from openpilot.common.params import Params from openpilot.common.timeout import Timeout -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI from openpilot.system.manager.process_config import managed_processes from openpilot.tools.lib.logreader import LogReader -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths SEGMENT_LENGTH = 2 FULL_SIZE = 2507572 @@ -51,9 +49,8 @@ class TestEncoder: return os.path.join(Paths.log_root(), last_route) # TODO: this should run faster than real time - @parameterized.expand([(True, ), (False, )]) - def test_log_rotation(self, record_front): - Params().put_bool("RecordFront", record_front, block=True) + def test_log_rotation(self): + Params().put_bool("RecordFront", True, block=True) managed_processes['sensord'].start() managed_processes['loggerd'].start() @@ -62,7 +59,7 @@ class TestEncoder: time.sleep(1.0) managed_processes['camerad'].start() - num_segments = int(os.getenv("SEGMENTS", random.randint(2, 8))) + num_segments = 3 # wait for loggerd to make the dir for first segment route_prefix_path = None @@ -78,9 +75,6 @@ class TestEncoder: counts = [] first_frames = [] for camera, fps, size_lambda, encode_idx_name in CAMERAS: - if not record_front and "dcamera" in camera: - continue - file_path = f"{route_prefix_path}--{i}/{camera}" # check file exists diff --git a/system/loggerd/tests/test_logger.cc b/openpilot/system/loggerd/tests/test_logger.cc similarity index 100% rename from system/loggerd/tests/test_logger.cc rename to openpilot/system/loggerd/tests/test_logger.cc diff --git a/system/loggerd/tests/test_loggerd.py b/openpilot/system/loggerd/tests/test_loggerd.py similarity index 95% rename from system/loggerd/tests/test_loggerd.py rename to openpilot/system/loggerd/tests/test_loggerd.py index 9fd1b2d434..3678ca95be 100644 --- a/system/loggerd/tests/test_loggerd.py +++ b/openpilot/system/loggerd/tests/test_loggerd.py @@ -5,22 +5,23 @@ import random import string import subprocess import time +from collections.abc import Collection from collections import defaultdict from pathlib import Path import pytest -import cereal.messaging as messaging -from cereal import log -from cereal.services import SERVICE_LIST +import openpilot.cereal.messaging as messaging +from openpilot.cereal import log +from openpilot.cereal.services import SERVICE_LIST from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.timeout import Timeout -from openpilot.system.hardware.hw import Paths -from openpilot.system.hardware import TICI +from openpilot.common.hardware.hw import Paths +from openpilot.common.hardware import TICI from openpilot.system.loggerd.xattr_cache import getxattr from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_VALUE from openpilot.system.manager.process_config import managed_processes -from openpilot.system.version import get_version +from openpilot.common.version import get_version from openpilot.tools.lib.helpers import RE from openpilot.tools.lib.logreader import LogReader from msgq.visionipc import VisionIpcServer, VisionStreamType @@ -54,7 +55,7 @@ class TestLoggerd: def _gen_bootlog(self): with Timeout(5): - out = subprocess.check_output("./bootlog", cwd=os.path.join(BASEDIR, "system/loggerd"), encoding='utf-8') + out = subprocess.check_output("./bootlog", cwd=os.path.join(BASEDIR, "openpilot/system/loggerd"), encoding='utf-8') log_fn = self._get_log_fn(out) @@ -74,8 +75,8 @@ class TestLoggerd: end_type = SentinelType.endOfRoute if route else SentinelType.endOfSegment assert msgs[-1].sentinel.type == end_type - def _publish_random_messages(self, services: list[str]) -> dict[str, list]: - pm = messaging.PubMaster(services) + def _publish_random_messages(self, services: Collection[str]) -> dict[str, list]: + pm = messaging.PubMaster(list(services)) managed_processes["loggerd"].start() for s in services: @@ -277,7 +278,9 @@ class TestLoggerd: assert recv_cnt == 0, f"got {recv_cnt} {s} msgs in qlog" else: # check logged message count matches decimation - expected_cnt = (len(msgs) - 1) // SERVICE_LIST[s].decimation + 1 + decimation = SERVICE_LIST[s].decimation + assert decimation is not None + expected_cnt = (len(msgs) - 1) // decimation + 1 assert recv_cnt == expected_cnt, f"expected {expected_cnt} msgs for {s}, got {recv_cnt}" def test_rlog(self): diff --git a/system/loggerd/tests/test_runner.cc b/openpilot/system/loggerd/tests/test_runner.cc similarity index 100% rename from system/loggerd/tests/test_runner.cc rename to openpilot/system/loggerd/tests/test_runner.cc diff --git a/system/loggerd/tests/test_uploader.py b/openpilot/system/loggerd/tests/test_uploader.py similarity index 98% rename from system/loggerd/tests/test_uploader.py rename to openpilot/system/loggerd/tests/test_uploader.py index 562bc068eb..3af78e2c37 100644 --- a/system/loggerd/tests/test_uploader.py +++ b/openpilot/system/loggerd/tests/test_uploader.py @@ -4,7 +4,7 @@ import threading import logging import json from pathlib import Path -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog from openpilot.system.loggerd.uploader import main, UPLOAD_ATTR_NAME, UPLOAD_ATTR_VALUE @@ -18,8 +18,8 @@ class FakeLogHandler(logging.Handler): self.reset() def reset(self): - self.upload_order = list() - self.upload_ignored = list() + self.upload_order = [] + self.upload_ignored = [] def emit(self, record): try: diff --git a/system/loggerd/tests/test_zstd_writer.cc b/openpilot/system/loggerd/tests/test_zstd_writer.cc similarity index 100% rename from system/loggerd/tests/test_zstd_writer.cc rename to openpilot/system/loggerd/tests/test_zstd_writer.cc diff --git a/system/loggerd/tests/vidc_debug.sh b/openpilot/system/loggerd/tests/vidc_debug.sh similarity index 100% rename from system/loggerd/tests/vidc_debug.sh rename to openpilot/system/loggerd/tests/vidc_debug.sh diff --git a/system/loggerd/uploader.py b/openpilot/system/loggerd/uploader.py similarity index 96% rename from system/loggerd/uploader.py rename to openpilot/system/loggerd/uploader.py index 8ac38b6df6..81f8ed1ba3 100755 --- a/system/loggerd/uploader.py +++ b/openpilot/system/loggerd/uploader.py @@ -9,13 +9,13 @@ import traceback import datetime from collections.abc import Iterator -from cereal import log -import cereal.messaging as messaging +from openpilot.cereal import log +import openpilot.cereal.messaging as messaging from openpilot.common.api import Api from openpilot.common.utils import get_upload_stream from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.system.loggerd.xattr_cache import getxattr, setxattr from openpilot.common.swaglog import cloudlog @@ -46,9 +46,7 @@ class FakeResponse: def get_directory_sort(d: str) -> list[str]: - # ensure old format is sorted sooner - o = ["0", ] if d.startswith("2024-") else ["1", ] - return o + [s.rjust(10, '0') for s in d.rsplit('--', 1)] + return [s.rjust(10, '0') for s in d.rsplit('--', 1)] def listdir_by_creation(d: str) -> list[str]: if not os.path.isdir(d): diff --git a/system/loggerd/video_writer.cc b/openpilot/system/loggerd/video_writer.cc similarity index 100% rename from system/loggerd/video_writer.cc rename to openpilot/system/loggerd/video_writer.cc diff --git a/system/loggerd/video_writer.h b/openpilot/system/loggerd/video_writer.h similarity index 95% rename from system/loggerd/video_writer.h rename to openpilot/system/loggerd/video_writer.h index e973c5d811..fdec606058 100644 --- a/system/loggerd/video_writer.h +++ b/openpilot/system/loggerd/video_writer.h @@ -8,7 +8,7 @@ extern "C" { #include } -#include "cereal/messaging/messaging.h" +#include "openpilot/cereal/messaging/messaging.h" class VideoWriter { public: diff --git a/openpilot/system/loggerd/xattr_cache.py b/openpilot/system/loggerd/xattr_cache.py new file mode 100644 index 0000000000..e0f6ba9588 --- /dev/null +++ b/openpilot/system/loggerd/xattr_cache.py @@ -0,0 +1,70 @@ +import ctypes +import errno +import os +import sys + + +if sys.platform == "darwin": + _libc = ctypes.CDLL(None, use_errno=True) + _libc.getxattr.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32, ctypes.c_int] + _libc.getxattr.restype = ctypes.c_ssize_t + _libc.setxattr.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32, ctypes.c_int] + _libc.setxattr.restype = ctypes.c_int + + +def _raise_os_error(path: str) -> None: + error = ctypes.get_errno() + raise OSError(error, os.strerror(error), path) + + +def _getxattr(path: str, attr_name: str) -> bytes: + if sys.platform != "darwin": + return os.getxattr(path, attr_name) + + encoded_path = os.fsencode(path) + encoded_attr_name = os.fsencode(attr_name) + while True: + size = _libc.getxattr(encoded_path, encoded_attr_name, None, 0, 0, 0) + if size == -1: + _raise_os_error(path) + if size == 0: + return b"" + + value = ctypes.create_string_buffer(size) + result = _libc.getxattr(encoded_path, encoded_attr_name, value, size, 0, 0) + if result != -1: + return value.raw[:result] + if ctypes.get_errno() != errno.ERANGE: + _raise_os_error(path) + + +def _setxattr(path: str, attr_name: str, attr_value: bytes) -> None: + if sys.platform != "darwin": + os.setxattr(path, attr_name, attr_value) + return + + encoded_path = os.fsencode(path) + encoded_attr_name = os.fsencode(attr_name) + value = ctypes.create_string_buffer(attr_value) + if _libc.setxattr(encoded_path, encoded_attr_name, value, len(attr_value), 0, 0) == -1: + _raise_os_error(path) + +_cached_attributes: dict[tuple, bytes | None] = {} + +def getxattr(path: str, attr_name: str) -> bytes | None: + key = (path, attr_name) + if key not in _cached_attributes: + try: + response = _getxattr(path, attr_name) + except OSError as e: + # ENODATA (Linux) or ENOATTR (macOS) means attribute hasn't been set + if e.errno == errno.ENODATA or (hasattr(errno, 'ENOATTR') and e.errno == errno.ENOATTR): + response = None + else: + raise + _cached_attributes[key] = response + return _cached_attributes[key] + +def setxattr(path: str, attr_name: str, attr_value: bytes) -> None: + _cached_attributes.pop((path, attr_name), None) + _setxattr(path, attr_name, attr_value) diff --git a/system/loggerd/zstd_writer.cc b/openpilot/system/loggerd/zstd_writer.cc similarity index 100% rename from system/loggerd/zstd_writer.cc rename to openpilot/system/loggerd/zstd_writer.cc diff --git a/system/loggerd/zstd_writer.h b/openpilot/system/loggerd/zstd_writer.h similarity index 100% rename from system/loggerd/zstd_writer.h rename to openpilot/system/loggerd/zstd_writer.h diff --git a/system/logmessaged.py b/openpilot/system/logmessaged.py similarity index 93% rename from system/logmessaged.py rename to openpilot/system/logmessaged.py index c095c26192..937a0741d0 100755 --- a/system/logmessaged.py +++ b/openpilot/system/logmessaged.py @@ -2,9 +2,9 @@ import zmq from typing import NoReturn -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.common.logging_extra import SwagLogFileFormatter -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import get_file_handler diff --git a/system/hardware/tici/__init__.py b/openpilot/system/manager/__init__.py similarity index 100% rename from system/hardware/tici/__init__.py rename to openpilot/system/manager/__init__.py diff --git a/system/manager/build.py b/openpilot/system/manager/build.py similarity index 97% rename from system/manager/build.py rename to openpilot/system/manager/build.py index 470cfccdaa..75a7dc63eb 100755 --- a/system/manager/build.py +++ b/openpilot/system/manager/build.py @@ -6,7 +6,7 @@ import subprocess from openpilot.common.basedir import BASEDIR from openpilot.common.spinner import Spinner from openpilot.common.text_window import TextWindow -from openpilot.system.hardware import HARDWARE, AGNOS +from openpilot.common.hardware import HARDWARE, AGNOS def build() -> None: spinner = Spinner() diff --git a/system/manager/github_runner.sh b/openpilot/system/manager/github_runner.sh similarity index 100% rename from system/manager/github_runner.sh rename to openpilot/system/manager/github_runner.sh diff --git a/system/manager/helpers.py b/openpilot/system/manager/helpers.py similarity index 87% rename from system/manager/helpers.py rename to openpilot/system/manager/helpers.py index 6af4a799e4..453e13184d 100644 --- a/system/manager/helpers.py +++ b/openpilot/system/manager/helpers.py @@ -44,12 +44,6 @@ def unblock_stdout() -> None: exit_status = os.wait()[1] >> 8 os._exit(exit_status) - -def write_onroad_params(started, params): - params.put_bool("IsOnroad", started, block=True) - params.put_bool("IsOffroad", not started, block=True) - - def save_bootlog(): # copy current params tmp = tempfile.mkdtemp() @@ -60,7 +54,7 @@ def save_bootlog(): def fn(tmpdir): env = os.environ.copy() env['PARAMS_COPY_PATH'] = tmpdir - subprocess.call("./bootlog", cwd=os.path.join(BASEDIR, "system/loggerd"), env=env) + subprocess.call("./bootlog", cwd=os.path.join(BASEDIR, "openpilot/system/loggerd"), env=env) shutil.rmtree(tmpdir) t = threading.Thread(target=fn, args=(tmp, )) t.daemon = True diff --git a/system/manager/manager.py b/openpilot/system/manager/manager.py similarity index 93% rename from system/manager/manager.py rename to openpilot/system/manager/manager.py index 0eb55c2727..7befb7f4ae 100755 --- a/system/manager/manager.py +++ b/openpilot/system/manager/manager.py @@ -6,21 +6,20 @@ import sys import time import traceback -from cereal import log -import cereal.messaging as messaging +from openpilot.cereal import log +import openpilot.cereal.messaging as messaging import openpilot.system.sentry as sentry from openpilot.common.utils import atomic_write from openpilot.common.params import Params, ParamKeyFlag from openpilot.common.text_window import TextWindow -from openpilot.system.hardware import HARDWARE -from openpilot.system.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog +from openpilot.common.hardware import HARDWARE, PC +from openpilot.system.manager.helpers import unblock_stdout, save_bootlog from openpilot.system.manager.process import ensure_running from openpilot.system.manager.process_config import managed_processes from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_ID 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 +from openpilot.common.version import get_build_metadata +from openpilot.common.hardware.hw import Paths from openpilot.sunnypilot.system.params_migration import run_migration @@ -105,11 +104,6 @@ def manager_init() -> None: dirty=build_metadata.openpilot.is_dirty, device=HARDWARE.get_device_type()) - # preimport all processes - for p in managed_processes.values(): - p.prepare() - - def manager_cleanup() -> None: # send signals to kill all procs for p in managed_processes.values(): @@ -139,7 +133,7 @@ def manager_thread() -> None: sm = messaging.SubMaster(['deviceState', 'carParams', 'pandaStates'], poll='deviceState') pm = messaging.PubMaster(['managerState']) - write_onroad_params(False, params) + params.put_bool("IsOffroad", True, block=True) ensure_running(managed_processes.values(), False, params=params, CP=sm['carParams'], not_run=ignore) started_prev = False @@ -159,9 +153,9 @@ def manager_thread() -> None: if ignition and not ignition_prev: params.clear_all(ParamKeyFlag.CLEAR_ON_IGNITION_ON) - # update onroad params, which drives pandad's safety setter thread + # update offroad state for services that don't subscribe to deviceState if started != started_prev: - write_onroad_params(started, params) + params.put_bool("IsOffroad", not started, block=True) started_prev = started ignition_prev = ignition diff --git a/system/manager/process.py b/openpilot/system/manager/process.py similarity index 95% rename from system/manager/process.py rename to openpilot/system/manager/process.py index cdb316e1a1..7f7eabd7be 100644 --- a/system/manager/process.py +++ b/openpilot/system/manager/process.py @@ -9,8 +9,9 @@ from multiprocessing import Process from setproctitle import setproctitle -from cereal import car, log -import cereal.messaging as messaging +from openpilot.cereal import log +from opendbc.car.structs import car +import openpilot.cereal.messaging as messaging import openpilot.system.sentry as sentry from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params @@ -69,10 +70,6 @@ class ManagerProcess(ABC): shutting_down = False restart_if_crash = False - @abstractmethod - def prepare(self) -> None: - pass - @abstractmethod def start(self) -> None: pass @@ -149,9 +146,6 @@ class NativeProcess(ManagerProcess): self.sigkill = sigkill self.launcher = nativelauncher - def prepare(self) -> None: - pass - def start(self) -> None: # In case we only tried a non blocking stop we need to stop it before restarting if self.shutting_down: @@ -177,11 +171,6 @@ class PythonProcess(ManagerProcess): self.launcher = launcher self.restart_if_crash = restart_if_crash - def prepare(self) -> None: - if self.enabled: - cloudlog.info(f"preimporting {self.module}") - importlib.import_module(self.module) - def start(self) -> None: # In case we only tried a non blocking stop we need to stop it before restarting if self.shutting_down: @@ -210,9 +199,6 @@ class DaemonProcess(ManagerProcess): def should_run(started, params, CP): return True - def prepare(self) -> None: - pass - def start(self) -> None: if self.params is None: self.params = Params() diff --git a/openpilot/system/manager/process_config.py b/openpilot/system/manager/process_config.py new file mode 100644 index 0000000000..a72675385d --- /dev/null +++ b/openpilot/system/manager/process_config.py @@ -0,0 +1,211 @@ +import os +import operator +import platform + +from opendbc.car.structs import car +from openpilot.cereal import custom +from openpilot.common.params import Params +from openpilot.common.hardware import PC, TICI +from openpilot.system.manager.process import PythonProcess, NativeProcess, DaemonProcess +from openpilot.common.hardware.hw import Paths + +from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH + +from openpilot.sunnypilot.models.helpers import get_active_model_runner +from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, use_sunnylink_uploader + +WEBCAM = os.getenv("USE_WEBCAM") is not None + +def driverview(started: bool, params: Params, CP: car.CarParams) -> bool: + return started or params.get_bool("IsDriverViewEnabled") + +def notcar(started: bool, params: Params, CP: car.CarParams) -> bool: + return started and CP.notCar + +def iscar(started: bool, params: Params, CP: car.CarParams) -> bool: + return started and not CP.notCar + +def logging(started: bool, params: Params, CP: car.CarParams) -> bool: + run = (not CP.notCar) or not params.get_bool("DisableLogging") + return started and run + +def ublox_available() -> bool: + return os.path.exists('/dev/ttyHS0') and not os.path.exists('/persist/comma/use-quectel-gps') + +def ublox(started: bool, params: Params, CP: car.CarParams) -> bool: + use_ublox = ublox_available() + if use_ublox != params.get_bool("UbloxAvailable"): + params.put_bool("UbloxAvailable", use_ublox, block=True) + return started and use_ublox + +def joystick(started: bool, params: Params, CP: car.CarParams) -> bool: + return started and params.get_bool("JoystickDebugMode") + +def not_joystick(started: bool, params: Params, CP: car.CarParams) -> bool: + return started and not params.get_bool("JoystickDebugMode") + +def long_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool: + return started and params.get_bool("LongitudinalManeuverMode") + +def lat_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool: + return started and params.get_bool("LateralManeuverMode") + +def not_long_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool: + return started and not params.get_bool("LongitudinalManeuverMode") + +def qcomgps(started: bool, params: Params, CP: car.CarParams) -> bool: + return started and not ublox_available() + +def always_run(started: bool, params: Params, CP: car.CarParams) -> bool: + return True + +def only_onroad(started: bool, params: Params, CP: car.CarParams) -> bool: + return started + +def only_offroad(started: bool, params: Params, CP: car.CarParams) -> bool: + return not started + +def livestream(started: bool, params: Params, CP: car.CarParams) -> bool: + return params.get_bool("IsLiveStreaming") + +def use_github_runner(started, params, CP: car.CarParams) -> bool: + return not PC and params.get_bool("EnableGithubRunner") and ( + not params.get_bool("NetworkMetered") and not params.get_bool("GithubRunnerSufficientVoltage")) + +def use_copyparty(started, params, CP: car.CarParams) -> bool: + return bool(params.get_bool("EnableCopyparty")) + +def sunnylink_ready_shim(started, params, CP: car.CarParams) -> bool: + """Shim for sunnylink_ready to match the process manager signature.""" + return sunnylink_ready(params) + +def sunnylink_need_register_shim(started, params, CP: car.CarParams) -> bool: + """Shim for sunnylink_need_register to match the process manager signature.""" + return sunnylink_need_register(params) + +def use_sunnylink_uploader_shim(started, params, CP: car.CarParams) -> bool: + """Shim for use_sunnylink_uploader to match the process manager signature.""" + return use_sunnylink_uploader(params) + +def is_tinygrad_model(started, params, CP: car.CarParams) -> bool: + """Check if the active model runner is SNPE.""" + return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.tinygrad) + +def is_stock_model(started, params, CP: car.CarParams) -> bool: + """Check if the active model runner is stock.""" + return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.stock) + +def mapd_ready(started: bool, params: Params, CP: car.CarParams) -> bool: + return bool(os.path.exists(Paths.mapd_root())) + +def uploader_ready(started: bool, params: Params, CP: car.CarParams) -> bool: + if not params.get_bool("OnroadUploads"): + return only_offroad(started, params, CP) + + return always_run(started, params, CP) + +def or_(*fns): + return lambda *args: operator.or_(*(fn(*args) for fn in fns)) + +def and_(*fns): + return lambda *args: operator.and_(*(fn(*args) for fn in fns)) + +def not_(*fns): + return lambda *args: operator.not_(*(fn(*args) for fn in fns)) + +procs = [ + DaemonProcess("manage_athenad", "openpilot.system.athena.manage_athenad", "AthenadPid"), + + NativeProcess("loggerd", "openpilot/system/loggerd", ["./loggerd"], logging), + NativeProcess("encoderd", "openpilot/system/loggerd", ["./encoderd"], only_onroad), + NativeProcess("stream_encoderd", "openpilot/system/loggerd", ["./encoderd", "--stream"], or_(and_(livestream, not_(iscar)), notcar)), + PythonProcess("logmessaged", "openpilot.system.logmessaged", always_run), + + NativeProcess("camerad", "openpilot/system/camerad", ["./camerad"], or_(driverview, livestream), enabled=not WEBCAM), + PythonProcess("webcamerad", "openpilot.system.camerad.webcam.camerad", driverview, enabled=WEBCAM), + PythonProcess("proclogd", "openpilot.system.proclogd", only_onroad, enabled=platform.system() != "Darwin"), + PythonProcess("journald", "openpilot.system.journald", only_onroad, platform.system() != "Darwin"), + PythonProcess("micd", "openpilot.system.micd", iscar), + PythonProcess("timed", "openpilot.system.timed", always_run, enabled=not PC), + + PythonProcess("modeld", "openpilot.selfdrive.modeld.modeld", and_(only_onroad, is_stock_model)), + PythonProcess("dmonitoringmodeld", "openpilot.selfdrive.modeld.dmonitoringmodeld", driverview, enabled=(WEBCAM or not PC)), + + PythonProcess("sensord", "openpilot.system.sensord.sensord", only_onroad, enabled=not PC), + PythonProcess("ui", "openpilot.selfdrive.ui.ui", always_run, restart_if_crash=True), + PythonProcess("soundd", "openpilot.selfdrive.ui.soundd", driverview), + PythonProcess("locationd", "openpilot.selfdrive.locationd.locationd", only_onroad), + NativeProcess("_pandad", "openpilot/selfdrive/pandad", ["./pandad"], always_run, enabled=False), + PythonProcess("calibrationd", "openpilot.selfdrive.locationd.calibrationd", only_onroad), + PythonProcess("torqued", "openpilot.selfdrive.locationd.torqued", only_onroad), + PythonProcess("controlsd", "openpilot.selfdrive.controls.controlsd", and_(not_joystick, iscar)), + PythonProcess("joystickd", "openpilot.tools.joystick.joystickd", or_(joystick, notcar)), + PythonProcess("selfdrived", "openpilot.selfdrive.selfdrived.selfdrived", only_onroad), + PythonProcess("card", "openpilot.selfdrive.car.card", only_onroad), + PythonProcess("deleter", "openpilot.system.loggerd.deleter", always_run), + PythonProcess("dmonitoringd", "openpilot.selfdrive.monitoring.dmonitoringd", driverview, enabled=(WEBCAM or not PC)), + PythonProcess("qcomgpsd", "openpilot.system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI), + PythonProcess("pandad", "openpilot.selfdrive.pandad.pandad", always_run), + PythonProcess("paramsd", "openpilot.selfdrive.locationd.paramsd", only_onroad), + PythonProcess("lagd", "openpilot.selfdrive.locationd.lagd", only_onroad), + PythonProcess("ubloxd", "openpilot.system.ubloxd.ubloxd", ublox, enabled=TICI), + PythonProcess("pigeond", "openpilot.system.ubloxd.pigeond", ublox, enabled=TICI), + PythonProcess("plannerd", "openpilot.selfdrive.controls.plannerd", not_long_maneuver), + PythonProcess("maneuversd", "openpilot.tools.longitudinal_maneuvers.maneuversd", long_maneuver), + PythonProcess("lateral_maneuversd", "openpilot.tools.lateral_maneuvers.lateral_maneuversd", lat_maneuver), + PythonProcess("radard", "openpilot.selfdrive.controls.radard", only_onroad), + PythonProcess("hardwared", "openpilot.system.hardware.hardwared", always_run), + PythonProcess("modem", "openpilot.common.hardware.tici.modem", always_run, enabled=TICI), + PythonProcess("tombstoned", "openpilot.system.tombstoned", always_run, enabled=not PC), + PythonProcess("updated", "openpilot.system.updated.updated", only_offroad, enabled=not PC), + PythonProcess("uploader", "openpilot.system.loggerd.uploader", uploader_ready), + PythonProcess("statsd", "openpilot.sunnypilot.system.statsd", always_run), + PythonProcess("feedbackd", "openpilot.selfdrive.ui.feedback.feedbackd", only_onroad), + + # debug procs + NativeProcess("bridge", "openpilot/cereal/messaging", ["./bridge"], notcar), + PythonProcess("webrtcd", "openpilot.system.webrtc.webrtcd", or_(and_(livestream, not_(iscar)), notcar)), + PythonProcess("joystick", "openpilot.tools.joystick.joystick_control", and_(joystick, iscar)), + + # sunnylink <3 + DaemonProcess("manage_sunnylinkd", "openpilot.sunnypilot.sunnylink.athena.manage_sunnylinkd", "SunnylinkdPid"), + PythonProcess("sunnylink_registration_manager", "openpilot.sunnypilot.sunnylink.registration_manager", sunnylink_need_register_shim), + PythonProcess("statsd_sp", "openpilot.sunnypilot.sunnylink.statsd", and_(always_run, sunnylink_ready_shim)), +] + +# sunnypilot +procs += [ + # Models + PythonProcess("models_manager", "openpilot.sunnypilot.models.manager", only_offroad), + NativeProcess("modeld_tinygrad", "openpilot/sunnypilot/modeld_v2", ["./modeld"], and_(only_onroad, is_tinygrad_model)), + + # Backup + PythonProcess("backup_manager", "openpilot.sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)), + + # mapd + NativeProcess("mapd", Paths.mapd_root(), ["bash", "-c", f"{MAPD_PATH} > /dev/null 2>&1"], mapd_ready), + PythonProcess("mapd_manager", "openpilot.sunnypilot.mapd.mapd_manager", always_run), + + # locationd + NativeProcess("locationd_llk", "openpilot/sunnypilot/selfdrive/locationd", ["./locationd"], only_onroad), +] + +if os.path.exists("./github_runner.sh"): + procs += [NativeProcess("github_runner_start", "openpilot/system/manager", + ["./github_runner.sh", "start"], and_(only_offroad, use_github_runner), sigkill=False)] + +if os.path.exists("../../sunnypilot/sunnylink/uploader.py"): + procs += [PythonProcess("sunnylink_uploader", "openpilot.sunnypilot.sunnylink.uploader", use_sunnylink_uploader_shim)] + +if os.path.exists("../../third_party/copyparty/copyparty-sfx.py"): + sunnypilot_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + copyparty_args = [f"-v{Paths.crash_log_root()}:/swaglogs:r"] + copyparty_args += [f"-v{Paths.log_root()}:/routes:r"] + copyparty_args += [f"-v{Paths.model_root()}:/models:rw"] + copyparty_args += [f"-v{sunnypilot_root}:/sunnypilot:rw"] + copyparty_args += ["-p8080"] + copyparty_args += ["-z"] + copyparty_args += ["-q"] + procs += [NativeProcess("copyparty-sfx", "openpilot/third_party/copyparty", ["./copyparty-sfx.py", *copyparty_args], and_(only_offroad, use_copyparty))] + +managed_processes = {p.name: p for p in procs} diff --git a/system/hardware/tici/tests/__init__.py b/openpilot/system/manager/test/__init__.py similarity index 100% rename from system/hardware/tici/tests/__init__.py rename to openpilot/system/manager/test/__init__.py diff --git a/system/manager/test/test_manager.py b/openpilot/system/manager/test/test_manager.py similarity index 93% rename from system/manager/test/test_manager.py rename to openpilot/system/manager/test/test_manager.py index 34d07c6724..a3808bf29f 100644 --- a/system/manager/test/test_manager.py +++ b/openpilot/system/manager/test/test_manager.py @@ -3,12 +3,12 @@ import pytest import signal import time -from cereal import car +from opendbc.car.structs import car from openpilot.common.params import Params import openpilot.system.manager.manager as manager from openpilot.system.manager.process import ensure_running from openpilot.system.manager.process_config import managed_processes, procs -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE os.environ['FAKEUPLOAD'] = "1" @@ -27,10 +27,6 @@ class TestManager: def teardown_method(self): manager.manager_cleanup() - def test_manager_prepare(self): - os.environ['PREPAREONLY'] = '1' - manager.main() - def test_duplicate_procs(self): assert len(procs) == len(managed_processes), "Duplicate process names" diff --git a/system/micd.py b/openpilot/system/micd.py similarity index 99% rename from system/micd.py rename to openpilot/system/micd.py index 9b3ccc8d29..a3a93c8c1a 100755 --- a/system/micd.py +++ b/openpilot/system/micd.py @@ -3,7 +3,7 @@ import numpy as np from functools import cache import threading -from cereal import messaging +from openpilot.cereal import messaging from openpilot.common.realtime import Ratekeeper from openpilot.common.utils import retry from openpilot.common.swaglog import cloudlog diff --git a/system/proclogd.py b/openpilot/system/proclogd.py similarity index 99% rename from system/proclogd.py rename to openpilot/system/proclogd.py index b008f8ed9b..9979b3a401 100755 --- a/system/proclogd.py +++ b/openpilot/system/proclogd.py @@ -2,7 +2,7 @@ import os from typing import NoReturn, TypedDict -from cereal import messaging +from openpilot.cereal import messaging from openpilot.common.realtime import Ratekeeper from openpilot.common.swaglog import cloudlog diff --git a/system/qcomgpsd/modemdiag.py b/openpilot/system/qcomgpsd/modemdiag.py similarity index 80% rename from system/qcomgpsd/modemdiag.py rename to openpilot/system/qcomgpsd/modemdiag.py index 5d72aeba9e..313563a314 100644 --- a/system/qcomgpsd/modemdiag.py +++ b/openpilot/system/qcomgpsd/modemdiag.py @@ -1,8 +1,31 @@ import select -from serial import Serial -from crcmod import mkCrcFun from struct import pack, unpack_from, calcsize +from openpilot.common.serial import Serial + + +def _gen_crc16_reflected_table(poly: int) -> list[int]: + # poly is the reflected form (e.g. 0x8408 for CRC-16 poly 0x1021) + table = [] + for i in range(256): + crc = i + for _ in range(8): + if crc & 1: + crc = (crc >> 1) ^ poly + else: + crc >>= 1 + table.append(crc) + return table + +# CRC-16/IBM-SDLC (aka X-25 / ISO-HDLC): poly 0x1021, refin/refout, init/xor 0xFFFF +_CRC16_X25_TABLE = _gen_crc16_reflected_table(0x8408) + +def crc16_x25(data: bytes) -> int: + crc = 0xFFFF + for b in data: + crc = _CRC16_X25_TABLE[(crc ^ b) & 0xFF] ^ (crc >> 8) + return crc ^ 0xFFFF + class ModemDiag: def __init__(self): self.serial = self.open_serial() @@ -15,12 +38,11 @@ class ModemDiag: serial.reset_output_buffer() return serial - ccitt_crc16 = mkCrcFun(0x11021, initCrc=0, xorOut=0xffff) ESCAPE_CHAR = b'\x7d' TRAILER_CHAR = b'\x7e' def hdlc_encapsulate(self, payload): - payload += pack(' 'GnssClockNmeaPort': + ints = [int(value) if value else None for value in values[:5]] + floats = [float(value) if value else None for value in values[5:9]] + return cls(*ints, *floats) @dataclass class GnssMeasNmeaPort: - messageCount: int - messageNum: int - svCount: int + messageCount: int | None + messageNum: int | None + svCount: int | None # constellation enum: # 1 = GPS # 2 = SBAS @@ -44,10 +45,10 @@ class GnssMeasNmeaPort: # 4 = QZSS # 5 = BEIDOU # 6 = GALILEO - constellation: int - svId: int - flags: int # always zero - time_offset_ns: int + constellation: int | None + svId: int | None + flags: int | None # always zero + time_offset_ns: int | None # state bit mask: # 0x0001 = CODE LOCK # 0x0002 = BIT SYNC @@ -63,17 +64,18 @@ class GnssMeasNmeaPort: # 0x0800 = GALILEO E1C 2ND CODE LOCK # 0x1000 = GALILEO E1B PAGE SYNC # 0x2000 = GALILEO E1B PAGE SYNC - state: int - time_of_week_ns: int - time_of_week_uncertainty_ns: int # 1-sigma - carrier_to_noise_ratio: float - pseudorange_rate: float - pseudorange_rate_uncertainty: float # 1-sigma + state: int | None + time_of_week_ns: int | None + time_of_week_uncertainty_ns: int | None # 1-sigma + carrier_to_noise_ratio: float | None + pseudorange_rate: float | None + pseudorange_rate_uncertainty: float | None # 1-sigma - def __post_init__(self): - for field in fields(self): - val = getattr(self, field.name) - setattr(self, field.name, field.type(val) if val else None) + @classmethod + def from_fields(cls, values: list[str]) -> 'GnssMeasNmeaPort': + ints = [int(value) if value else None for value in values[:10]] + floats = [float(value) if value else None for value in values[10:13]] + return cls(*ints, *floats) def nmea_checksum_ok(s): checksum = 0 @@ -107,11 +109,11 @@ def process_nmea_port_messages(device:str="/dev/ttyUSB1") -> NoReturn: match fields[0]: case "$GNCLK": # fields at end are reserved (not used) - gnss_clock = GnssClockNmeaPort(*fields[1:10]) + gnss_clock = GnssClockNmeaPort.from_fields(fields[1:10]) print(gnss_clock) case "$GNMEAS": # fields at end are reserved (not used) - gnss_meas = GnssMeasNmeaPort(*fields[1:14]) + gnss_meas = GnssMeasNmeaPort.from_fields(fields[1:14]) print(gnss_meas) except Exception as e: print(e) @@ -119,7 +121,7 @@ def process_nmea_port_messages(device:str="/dev/ttyUSB1") -> NoReturn: def main() -> NoReturn: from openpilot.common.gpio import gpio_init, gpio_set - from openpilot.system.hardware.tici.pins import GPIO + from openpilot.common.hardware.tici.pins import GPIO from openpilot.system.qcomgpsd.qcomgpsd import at_cmd try: diff --git a/system/qcomgpsd/qcomgpsd.py b/openpilot/system/qcomgpsd/qcomgpsd.py similarity index 98% rename from system/qcomgpsd/qcomgpsd.py rename to openpilot/system/qcomgpsd/qcomgpsd.py index 7e0d304872..4e52154419 100755 --- a/system/qcomgpsd/qcomgpsd.py +++ b/openpilot/system/qcomgpsd/qcomgpsd.py @@ -6,17 +6,17 @@ import signal import itertools import math import time -from serial import Serial import datetime from typing import NoReturn from struct import unpack_from, calcsize, pack -from cereal import log -import cereal.messaging as messaging +from openpilot.cereal import log +import openpilot.cereal.messaging as messaging from openpilot.common.gpio import gpio_init, gpio_set from openpilot.common.utils import retry from openpilot.common.time_helpers import system_time_valid -from openpilot.system.hardware.tici.pins import GPIO +from openpilot.common.hardware.tici.pins import GPIO +from openpilot.common.serial import Serial from openpilot.common.swaglog import cloudlog from openpilot.system.qcomgpsd.modemdiag import ModemDiag, DIAG_LOG_F, setup_logs, send_recv from openpilot.system.qcomgpsd.structs import (dict_unpacker, position_report, relist, diff --git a/system/qcomgpsd/structs.py b/openpilot/system/qcomgpsd/structs.py similarity index 100% rename from system/qcomgpsd/structs.py rename to openpilot/system/qcomgpsd/structs.py diff --git a/system/loggerd/__init__.py b/openpilot/system/sensord/__init__.py similarity index 100% rename from system/loggerd/__init__.py rename to openpilot/system/sensord/__init__.py diff --git a/system/sensord/sensord.py b/openpilot/system/sensord/sensord.py similarity index 97% rename from system/sensord/sensord.py rename to openpilot/system/sensord/sensord.py index ce6fb9ccb2..6942b284df 100755 --- a/system/sensord/sensord.py +++ b/openpilot/system/sensord/sensord.py @@ -5,8 +5,8 @@ import ctypes import select import threading -import cereal.messaging as messaging -from cereal.services import SERVICE_LIST +import openpilot.cereal.messaging as messaging +from openpilot.cereal.services import SERVICE_LIST from openpilot.common.utils import sudo_write from openpilot.common.realtime import config_realtime_process, Ratekeeper from openpilot.common.swaglog import cloudlog diff --git a/system/loggerd/tests/__init__.py b/openpilot/system/sensord/sensors/__init__.py similarity index 100% rename from system/loggerd/tests/__init__.py rename to openpilot/system/sensord/sensors/__init__.py diff --git a/system/sensord/sensors/i2c_sensor.py b/openpilot/system/sensord/sensors/i2c_sensor.py similarity index 98% rename from system/sensord/sensors/i2c_sensor.py rename to openpilot/system/sensord/sensors/i2c_sensor.py index 57edcc52d9..e0e8087258 100644 --- a/system/sensord/sensors/i2c_sensor.py +++ b/openpilot/system/sensord/sensors/i2c_sensor.py @@ -2,7 +2,7 @@ import time import ctypes from collections.abc import Iterable -from cereal import log +from openpilot.cereal import log from openpilot.common.i2c import SMBus diff --git a/system/sensord/sensors/lsm6ds3_accel.py b/openpilot/system/sensord/sensors/lsm6ds3_accel.py similarity index 99% rename from system/sensord/sensors/lsm6ds3_accel.py rename to openpilot/system/sensord/sensors/lsm6ds3_accel.py index 761ae828bb..e47db15250 100644 --- a/system/sensord/sensors/lsm6ds3_accel.py +++ b/openpilot/system/sensord/sensors/lsm6ds3_accel.py @@ -1,7 +1,7 @@ import os import time -from cereal import log +from openpilot.cereal import log from openpilot.system.sensord.sensors.i2c_sensor import Sensor class LSM6DS3_Accel(Sensor): diff --git a/system/sensord/sensors/lsm6ds3_gyro.py b/openpilot/system/sensord/sensors/lsm6ds3_gyro.py similarity index 99% rename from system/sensord/sensors/lsm6ds3_gyro.py rename to openpilot/system/sensord/sensors/lsm6ds3_gyro.py index 654cff9da8..a82827ccb4 100644 --- a/system/sensord/sensors/lsm6ds3_gyro.py +++ b/openpilot/system/sensord/sensors/lsm6ds3_gyro.py @@ -2,7 +2,7 @@ import os import math import time -from cereal import log +from openpilot.cereal import log from openpilot.system.sensord.sensors.i2c_sensor import Sensor class LSM6DS3_Gyro(Sensor): diff --git a/system/sensord/sensors/lsm6ds3_temp.py b/openpilot/system/sensord/sensors/lsm6ds3_temp.py similarity index 96% rename from system/sensord/sensors/lsm6ds3_temp.py rename to openpilot/system/sensord/sensors/lsm6ds3_temp.py index ffe970c22c..0a5e85a2b9 100644 --- a/system/sensord/sensors/lsm6ds3_temp.py +++ b/openpilot/system/sensord/sensors/lsm6ds3_temp.py @@ -1,6 +1,6 @@ import time -from cereal import log +from openpilot.cereal import log from openpilot.system.sensord.sensors.i2c_sensor import Sensor # https://content.arduino.cc/assets/st_imu_lsm6ds3_datasheet.pdf diff --git a/system/manager/__init__.py b/openpilot/system/sensord/tests/__init__.py similarity index 100% rename from system/manager/__init__.py rename to openpilot/system/sensord/tests/__init__.py diff --git a/system/sensord/tests/test_sensord.py b/openpilot/system/sensord/tests/test_sensord.py similarity index 94% rename from system/sensord/tests/test_sensord.py rename to openpilot/system/sensord/tests/test_sensord.py index 3d7d26f9fa..dc96886e4a 100644 --- a/system/sensord/tests/test_sensord.py +++ b/openpilot/system/sensord/tests/test_sensord.py @@ -1,11 +1,12 @@ import os +import subprocess import pytest import time import numpy as np from collections import namedtuple, defaultdict -import cereal.messaging as messaging -from cereal.services import SERVICE_LIST +import openpilot.cereal.messaging as messaging +from openpilot.cereal.services import SERVICE_LIST from openpilot.common.gpio import get_irqs_for_action from openpilot.common.timeout import Timeout from openpilot.system.manager.process_config import managed_processes @@ -14,12 +15,12 @@ SensorConfig = namedtuple('SensorConfig', ['service', 'measurement', 'sanity_min SENSOR_CONFIGS = ( SensorConfig("accelerometer", "acceleration", 5, 15, 5), - SensorConfig("gyroscope", "gyroUncalibrated", 0, .15, 0.5), - SensorConfig("temperatureSensor", "temperature", 10, 40, 0.5), # set for max range of our office + SensorConfig("gyroscope", "gyroUncalibrated", 0, .3, 0.5), + SensorConfig("temperatureSensor", "temperature", 10, 60, 0.5), # looser for device density ) SENSOR_CONFIGS_BY_MEASUREMENT = {config.measurement: config for config in SENSOR_CONFIGS} -def get_irq_count(irq: int): +def get_irq_count(irq: str): with open(f"/sys/kernel/irq/{irq}/per_cpu_count") as f: per_cpu = map(int, f.read().split(",")) return sum(per_cpu) @@ -61,7 +62,7 @@ class TestSensord: os.environ["LSM_SELF_TEST"] = "1" # read initial sensor values every test case can use - os.system("pkill -f \\\\./sensord") + subprocess.run("pkill -f \\\\./sensord", shell=True) try: managed_processes["sensord"].start() cls.sample_secs = int(os.getenv("SAMPLE_SECS", "10")) diff --git a/system/sentry.py b/openpilot/system/sentry.py similarity index 96% rename from system/sentry.py rename to openpilot/system/sentry.py index a1fb604dea..dc078eb532 100644 --- a/system/sentry.py +++ b/openpilot/system/sentry.py @@ -8,10 +8,10 @@ from sentry_sdk.integrations.threading import ThreadingIntegration from openpilot.common.params import Params from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID -from openpilot.system.hardware import HARDWARE -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware import HARDWARE +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog -from openpilot.system.version import get_build_metadata, get_version +from openpilot.common.version import get_build_metadata, get_version from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID diff --git a/system/manager/test/__init__.py b/openpilot/system/tests/__init__.py similarity index 100% rename from system/manager/test/__init__.py rename to openpilot/system/tests/__init__.py diff --git a/system/tests/test_logmessaged.py b/openpilot/system/tests/test_logmessaged.py similarity index 94% rename from system/tests/test_logmessaged.py rename to openpilot/system/tests/test_logmessaged.py index 9ccc8ef53b..e2637fb0e6 100644 --- a/system/tests/test_logmessaged.py +++ b/openpilot/system/tests/test_logmessaged.py @@ -2,9 +2,9 @@ import glob import os import time -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.system.manager.process_config import managed_processes -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog, ipchandler diff --git a/system/timed.py b/openpilot/system/timed.py similarity index 97% rename from system/timed.py rename to openpilot/system/timed.py index c74ba51da5..0413f645e3 100755 --- a/system/timed.py +++ b/openpilot/system/timed.py @@ -4,7 +4,7 @@ import subprocess import time from typing import NoReturn -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.common.time_helpers import min_date, MAX_DATE, system_time_valid from openpilot.common.swaglog import cloudlog from openpilot.common.params import Params diff --git a/system/tombstoned.py b/openpilot/system/tombstoned.py similarity index 96% rename from system/tombstoned.py rename to openpilot/system/tombstoned.py index 5bcced2666..7741a923cd 100755 --- a/system/tombstoned.py +++ b/openpilot/system/tombstoned.py @@ -10,9 +10,9 @@ import glob from typing import NoReturn import openpilot.system.sentry as sentry -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog -from openpilot.system.version import get_build_metadata +from openpilot.common.version import get_build_metadata MAX_SIZE = 1_000_000 * 100 # allow up to 100M MAX_TOMBSTONE_FN_LEN = 62 # 85 - 23 ("/crash/") @@ -104,7 +104,7 @@ def report_tombstone_apport(fn): # Try to find first entry in openpilot, fall back to first line for line in stacktrace_s: - if "at selfdrive/" in line: + if "at openpilot/selfdrive/" in line or "at selfdrive/" in line: crash_function = line found = True break diff --git a/system/ubloxd/binary_struct.py b/openpilot/system/ubloxd/binary_struct.py similarity index 98% rename from system/ubloxd/binary_struct.py rename to openpilot/system/ubloxd/binary_struct.py index c144bd5696..5bc05094f3 100644 --- a/system/ubloxd/binary_struct.py +++ b/openpilot/system/ubloxd/binary_struct.py @@ -184,7 +184,7 @@ class BinaryStruct: setattr(obj, name, value) return obj - cls._read = _read + cls._read = _read # ty: ignore[invalid-assignment] # installed dynamically for each subclass @classmethod def from_bytes(cls: type[T], data: bytes) -> T: diff --git a/system/ubloxd/glonass.py b/openpilot/system/ubloxd/glonass.py similarity index 100% rename from system/ubloxd/glonass.py rename to openpilot/system/ubloxd/glonass.py diff --git a/system/ubloxd/gps.py b/openpilot/system/ubloxd/gps.py similarity index 100% rename from system/ubloxd/gps.py rename to openpilot/system/ubloxd/gps.py diff --git a/system/ubloxd/pigeond.py b/openpilot/system/ubloxd/pigeond.py similarity index 97% rename from system/ubloxd/pigeond.py rename to openpilot/system/ubloxd/pigeond.py index e458a9d65f..9891285465 100755 --- a/system/ubloxd/pigeond.py +++ b/openpilot/system/ubloxd/pigeond.py @@ -2,19 +2,19 @@ import sys import time import signal -import serial import struct import requests import urllib.parse from datetime import datetime, UTC -from cereal import messaging +from openpilot.cereal import messaging from openpilot.common.time_helpers import system_time_valid from openpilot.common.params import Params +from openpilot.common.serial import Serial from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI from openpilot.common.gpio import gpio_init, gpio_set -from openpilot.system.hardware.tici.pins import GPIO +from openpilot.common.hardware.tici.pins import GPIO UBLOX_TTY = "/dev/ttyHS0" @@ -64,7 +64,7 @@ def get_assistnow_messages(token: str) -> list[bytes]: class TTYPigeon: def __init__(self): - self.tty = serial.VTIMESerial(UBLOX_TTY, baudrate=9600, timeout=0) + self.tty = Serial(UBLOX_TTY, baudrate=9600, timeout=0) def send(self, dat: bytes) -> None: self.tty.write(dat) diff --git a/system/ubloxd/tests/test_pigeond.py b/openpilot/system/ubloxd/tests/test_pigeond.py similarity index 90% rename from system/ubloxd/tests/test_pigeond.py rename to openpilot/system/ubloxd/tests/test_pigeond.py index 202820e412..b894ed718b 100644 --- a/system/ubloxd/tests/test_pigeond.py +++ b/openpilot/system/ubloxd/tests/test_pigeond.py @@ -1,12 +1,12 @@ import pytest import time -import cereal.messaging as messaging -from cereal.services import SERVICE_LIST +import openpilot.cereal.messaging as messaging +from openpilot.cereal.services import SERVICE_LIST from openpilot.common.gpio import gpio_read from openpilot.selfdrive.test.helpers import with_processes from openpilot.system.manager.process_config import managed_processes -from openpilot.system.hardware.tici.pins import GPIO +from openpilot.common.hardware.tici.pins import GPIO # TODO: test TTFF when we have good A-GNSS diff --git a/system/ubloxd/ubloxd.py b/openpilot/system/ubloxd/ubloxd.py similarity index 99% rename from system/ubloxd/ubloxd.py rename to openpilot/system/ubloxd/ubloxd.py index 78429a847b..3b32f70657 100755 --- a/system/ubloxd/ubloxd.py +++ b/openpilot/system/ubloxd/ubloxd.py @@ -6,8 +6,8 @@ import numpy as np from collections import defaultdict from dataclasses import dataclass -from cereal import log -from cereal import messaging +from openpilot.cereal import log +from openpilot.cereal import messaging from openpilot.system.ubloxd.ubx import Ubx from openpilot.system.ubloxd.gps import Gps from openpilot.system.ubloxd.glonass import Glonass diff --git a/system/ubloxd/ubx.py b/openpilot/system/ubloxd/ubx.py similarity index 100% rename from system/ubloxd/ubx.py rename to openpilot/system/ubloxd/ubx.py diff --git a/system/ui/README.md b/openpilot/system/ui/README.md similarity index 91% rename from system/ui/README.md rename to openpilot/system/ui/README.md index 54697dada8..7c9713b39e 100644 --- a/system/ui/README.md +++ b/openpilot/system/ui/README.md @@ -16,6 +16,6 @@ Quick start: * https://electronstudio.github.io/raylib-python-cffi/README.html#quickstart Style guide: -* All graphical elements should subclass [`Widget`](/system/ui/widgets/__init__.py). +* All graphical elements should subclass [`Widget`](/openpilot/system/ui/widgets/__init__.py). * Prefer a stateful widget over a function for easy migration from QT * All internal class variables and functions should be prefixed with `_` diff --git a/system/sensord/__init__.py b/openpilot/system/ui/lib/__init__.py similarity index 100% rename from system/sensord/__init__.py rename to openpilot/system/ui/lib/__init__.py diff --git a/system/ui/lib/application.py b/openpilot/system/ui/lib/application.py similarity index 99% rename from system/ui/lib/application.py rename to openpilot/system/ui/lib/application.py index 42c0db7545..58fcc15a22 100644 --- a/system/ui/lib/application.py +++ b/openpilot/system/ui/lib/application.py @@ -18,7 +18,7 @@ from pathlib import Path from typing import NamedTuple from importlib.resources import as_file, files from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import HARDWARE, PC +from openpilot.common.hardware import HARDWARE, PC from openpilot.system.ui.lib.multilang import multilang from openpilot.common.realtime import Ratekeeper @@ -831,6 +831,7 @@ class GuiApplication(GuiApplicationExt): import pstats self._render_profiler.disable() + assert self._render_profile_start_time is not None elapsed_ms = (time.monotonic() - self._render_profile_start_time) * 1e3 avg_frame_time = elapsed_ms / self._frame if self._frame > 0 else 0 diff --git a/system/ui/lib/egl.py b/openpilot/system/ui/lib/egl.py similarity index 100% rename from system/ui/lib/egl.py rename to openpilot/system/ui/lib/egl.py diff --git a/system/ui/lib/emoji.py b/openpilot/system/ui/lib/emoji.py similarity index 100% rename from system/ui/lib/emoji.py rename to openpilot/system/ui/lib/emoji.py diff --git a/system/ui/lib/multilang.py b/openpilot/system/ui/lib/multilang.py similarity index 98% rename from system/ui/lib/multilang.py rename to openpilot/system/ui/lib/multilang.py index c7d39e4794..8d79ec3eea 100644 --- a/system/ui/lib/multilang.py +++ b/openpilot/system/ui/lib/multilang.py @@ -10,7 +10,7 @@ try: except ImportError: Params = None -SYSTEM_UI_DIR = os.path.join(BASEDIR, "system", "ui") +SYSTEM_UI_DIR = os.path.join(BASEDIR, "openpilot/system", "ui") UI_DIR = files("openpilot.selfdrive.ui") TRANSLATIONS_DIR = UI_DIR.joinpath("translations") LANGUAGES_FILE = TRANSLATIONS_DIR.joinpath("languages.json") diff --git a/system/ui/lib/networkmanager.py b/openpilot/system/ui/lib/networkmanager.py similarity index 100% rename from system/ui/lib/networkmanager.py rename to openpilot/system/ui/lib/networkmanager.py diff --git a/system/ui/lib/scroll_panel.py b/openpilot/system/ui/lib/scroll_panel.py similarity index 100% rename from system/ui/lib/scroll_panel.py rename to openpilot/system/ui/lib/scroll_panel.py diff --git a/system/ui/lib/scroll_panel2.py b/openpilot/system/ui/lib/scroll_panel2.py similarity index 99% rename from system/ui/lib/scroll_panel2.py rename to openpilot/system/ui/lib/scroll_panel2.py index 7fae60119c..b6193672c4 100644 --- a/system/ui/lib/scroll_panel2.py +++ b/openpilot/system/ui/lib/scroll_panel2.py @@ -5,7 +5,7 @@ from collections.abc import Callable from enum import Enum from typing import cast from openpilot.system.ui.lib.application import gui_app, MouseEvent -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI from collections import deque MIN_VELOCITY = 10 # px/s, changes from auto scroll to steady state diff --git a/system/ui/lib/shader_polygon.py b/openpilot/system/ui/lib/shader_polygon.py similarity index 97% rename from system/ui/lib/shader_polygon.py rename to openpilot/system/ui/lib/shader_polygon.py index 94af35e157..de729d6aae 100644 --- a/system/ui/lib/shader_polygon.py +++ b/openpilot/system/ui/lib/shader_polygon.py @@ -152,7 +152,7 @@ class ShaderState: self.initialized = False -def _configure_shader_color(state: ShaderState, color: Optional[rl.Color], +def _configure_shader_color(state: ShaderState, color: Optional[rl.Color], # noqa: UP045 # rl.Color is a function, so `rl.Color | None` fails gradient: Gradient | None, origin_rect: rl.Rectangle): assert (color is not None) != (gradient is not None), "Either color or gradient must be provided" @@ -204,7 +204,7 @@ def triangulate(pts: np.ndarray) -> list[tuple[float, float]]: def draw_polygon(origin_rect: rl.Rectangle, points: np.ndarray, - color: Optional[rl.Color] = None, gradient: Gradient | None = None): + color: Optional[rl.Color] = None, gradient: Gradient | None = None): # noqa: UP045 # rl.Color is a function, so `rl.Color | None` fails """ Draw a ribbon polygon (two chains) with a triangle strip and gradient. diff --git a/system/ui/lib/tests/test_handle_state_change.py b/openpilot/system/ui/lib/tests/test_handle_state_change.py similarity index 100% rename from system/ui/lib/tests/test_handle_state_change.py rename to openpilot/system/ui/lib/tests/test_handle_state_change.py diff --git a/system/ui/lib/text_measure.py b/openpilot/system/ui/lib/text_measure.py similarity index 100% rename from system/ui/lib/text_measure.py rename to openpilot/system/ui/lib/text_measure.py diff --git a/system/ui/lib/utils.py b/openpilot/system/ui/lib/utils.py similarity index 85% rename from system/ui/lib/utils.py rename to openpilot/system/ui/lib/utils.py index 77035d0da0..e97b3ba9d9 100644 --- a/system/ui/lib/utils.py +++ b/openpilot/system/ui/lib/utils.py @@ -1,8 +1,9 @@ import pyray as rl +from collections.abc import Sequence class GuiStyleContext: - def __init__(self, styles: list[tuple[int, int, int]]): + def __init__(self, styles: Sequence[tuple[int, int, int]]): """styles is a list of tuples (control, prop, new_value)""" self.styles = styles self.prev_styles: list[tuple[int, int, int]] = [] diff --git a/system/ui/lib/wifi_manager.py b/openpilot/system/ui/lib/wifi_manager.py similarity index 99% rename from system/ui/lib/wifi_manager.py rename to openpilot/system/ui/lib/wifi_manager.py index 87f0ebda83..dedb341cd0 100644 --- a/system/ui/lib/wifi_manager.py +++ b/openpilot/system/ui/lib/wifi_manager.py @@ -257,7 +257,7 @@ class WifiManager: def add_callbacks(self, need_auth: Callable[[str], None] | None = None, activated: Callable[[], None] | None = None, - forgotten: Callable[[str], None] | None = None, + forgotten: Callable[[str | None], None] | None = None, networks_updated: Callable[[list[Network]], None] | None = None, disconnected: Callable[[], None] | None = None): if need_auth is not None: diff --git a/system/ui/lib/wrap_text.py b/openpilot/system/ui/lib/wrap_text.py similarity index 100% rename from system/ui/lib/wrap_text.py rename to openpilot/system/ui/lib/wrap_text.py diff --git a/system/ui/mici_reset.py b/openpilot/system/ui/mici_reset.py similarity index 93% rename from system/ui/mici_reset.py rename to openpilot/system/ui/mici_reset.py index 9cc6e7f3f8..97ecaf7024 100755 --- a/system/ui/mici_reset.py +++ b/openpilot/system/ui/mici_reset.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -import os +import subprocess import sys import time import threading @@ -7,7 +7,7 @@ from enum import IntEnum import pyray as rl -from openpilot.system.hardware import HARDWARE, PC +from openpilot.common.hardware import HARDWARE, PC from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.mici_setup import GreyBigButton, FailedPage @@ -105,12 +105,12 @@ class Reset(Scroller): return # Removing data and formatting - rm = os.system("sudo rm -rf /data/*") - os.system(f"sudo umount {USERDATA}") - fmt = os.system(f"yes | sudo mkfs.ext4 {USERDATA}") + rm = subprocess.run("sudo rm -rf /data/*", shell=True).returncode + subprocess.run(f"sudo umount {USERDATA}", shell=True) + fmt = subprocess.run(f"yes | sudo mkfs.ext4 {USERDATA}", shell=True).returncode if rm == 0 or fmt == 0: - os.system("sudo reboot") + subprocess.run("sudo reboot", shell=True) else: self._reset_failed = True diff --git a/system/ui/mici_setup.py b/openpilot/system/ui/mici_setup.py similarity index 99% rename from system/ui/mici_setup.py rename to openpilot/system/ui/mici_setup.py index 8f4541e9e6..e446697cfe 100755 --- a/system/ui/mici_setup.py +++ b/openpilot/system/ui/mici_setup.py @@ -11,9 +11,9 @@ from collections.abc import Callable import pyray as rl -from cereal import log +from openpilot.cereal import log from openpilot.common.filter_simple import BounceFilter -from openpilot.system.hardware import HARDWARE, TICI +from openpilot.common.hardware import HARDWARE, TICI from openpilot.common.realtime import config_realtime_process, set_core_affinity from openpilot.common.swaglog import cloudlog from openpilot.common.time_helpers import system_time_valid diff --git a/system/ui/mici_updater.py b/openpilot/system/ui/mici_updater.py similarity index 99% rename from system/ui/mici_updater.py rename to openpilot/system/ui/mici_updater.py index 8437e6fa60..a072354cf0 100755 --- a/system/ui/mici_updater.py +++ b/openpilot/system/ui/mici_updater.py @@ -5,7 +5,7 @@ import threading import pyray as rl from openpilot.common.realtime import config_realtime_process, set_core_affinity -from openpilot.system.hardware import HARDWARE, TICI +from openpilot.common.hardware import HARDWARE, TICI from openpilot.common.swaglog import cloudlog from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets.nav_widget import NavWidget diff --git a/system/ui/reset.py b/openpilot/system/ui/reset.py similarity index 100% rename from system/ui/reset.py rename to openpilot/system/ui/reset.py diff --git a/system/ui/setup.py b/openpilot/system/ui/setup.py similarity index 100% rename from system/ui/setup.py rename to openpilot/system/ui/setup.py diff --git a/system/ui/spinner.py b/openpilot/system/ui/spinner.py similarity index 100% rename from system/ui/spinner.py rename to openpilot/system/ui/spinner.py diff --git a/system/ui/sunnypilot/lib/application.py b/openpilot/system/ui/sunnypilot/lib/application.py similarity index 100% rename from system/ui/sunnypilot/lib/application.py rename to openpilot/system/ui/sunnypilot/lib/application.py diff --git a/system/ui/sunnypilot/lib/styles.py b/openpilot/system/ui/sunnypilot/lib/styles.py similarity index 100% rename from system/ui/sunnypilot/lib/styles.py rename to openpilot/system/ui/sunnypilot/lib/styles.py diff --git a/system/ui/sunnypilot/lib/utils.py b/openpilot/system/ui/sunnypilot/lib/utils.py similarity index 100% rename from system/ui/sunnypilot/lib/utils.py rename to openpilot/system/ui/sunnypilot/lib/utils.py diff --git a/system/ui/sunnypilot/widgets/__init__.py b/openpilot/system/ui/sunnypilot/widgets/__init__.py similarity index 100% rename from system/ui/sunnypilot/widgets/__init__.py rename to openpilot/system/ui/sunnypilot/widgets/__init__.py diff --git a/system/sensord/sensors/__init__.py b/openpilot/system/ui/sunnypilot/widgets/helpers/__init__.py similarity index 100% rename from system/sensord/sensors/__init__.py rename to openpilot/system/ui/sunnypilot/widgets/helpers/__init__.py diff --git a/system/ui/sunnypilot/widgets/helpers/fuzzy_search.py b/openpilot/system/ui/sunnypilot/widgets/helpers/fuzzy_search.py similarity index 100% rename from system/ui/sunnypilot/widgets/helpers/fuzzy_search.py rename to openpilot/system/ui/sunnypilot/widgets/helpers/fuzzy_search.py diff --git a/system/ui/sunnypilot/widgets/helpers/star_icon.py b/openpilot/system/ui/sunnypilot/widgets/helpers/star_icon.py similarity index 100% rename from system/ui/sunnypilot/widgets/helpers/star_icon.py rename to openpilot/system/ui/sunnypilot/widgets/helpers/star_icon.py diff --git a/system/ui/sunnypilot/widgets/html_render.py b/openpilot/system/ui/sunnypilot/widgets/html_render.py similarity index 100% rename from system/ui/sunnypilot/widgets/html_render.py rename to openpilot/system/ui/sunnypilot/widgets/html_render.py diff --git a/system/ui/sunnypilot/widgets/input_dialog.py b/openpilot/system/ui/sunnypilot/widgets/input_dialog.py similarity index 100% rename from system/ui/sunnypilot/widgets/input_dialog.py rename to openpilot/system/ui/sunnypilot/widgets/input_dialog.py diff --git a/system/ui/sunnypilot/widgets/list_view.py b/openpilot/system/ui/sunnypilot/widgets/list_view.py similarity index 98% rename from system/ui/sunnypilot/widgets/list_view.py rename to openpilot/system/ui/sunnypilot/widgets/list_view.py index 89342572d1..ff4b8c13dd 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/openpilot/system/ui/sunnypilot/widgets/list_view.py @@ -4,7 +4,7 @@ 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 collections.abc import Callable, Sequence import pyray as rl from openpilot.common.params import Params @@ -126,7 +126,7 @@ class DualButtonActionSP(DualButtonAction): class MultipleButtonActionSP(MultipleButtonAction): - def __init__(self, buttons: list[str | Callable[[], str]], button_width: int, selected_index: int = 0, callback: Callable | None = None, + def __init__(self, buttons: Sequence[str | Callable[[], str]], button_width: int, selected_index: int = 0, callback: Callable | None = None, param: str | None = None): MultipleButtonAction.__init__(self, buttons, button_width, selected_index, callback) self.param_key = param @@ -366,7 +366,7 @@ def toggle_item_sp(title: str | Callable[[], str], description: str | Callable[[ return ListItemSP(title=title, description=description, action_item=action, icon=icon) -def multiple_button_item_sp(title: str | Callable[[], str], description: str | Callable[[], str], buttons: list[str | Callable[[], str]], +def multiple_button_item_sp(title: str | Callable[[], str], description: str | Callable[[], str], buttons: Sequence[str | Callable[[], str]], selected_index: int = 0, button_width: int = style.BUTTON_ACTION_WIDTH, callback: Callable | None = None, icon: str = "", param: str | None = None, inline: bool = False) -> ListItemSP: action = MultipleButtonActionSP(buttons, button_width, selected_index, callback=callback, param=param) diff --git a/system/ui/sunnypilot/widgets/option_control.py b/openpilot/system/ui/sunnypilot/widgets/option_control.py similarity index 100% rename from system/ui/sunnypilot/widgets/option_control.py rename to openpilot/system/ui/sunnypilot/widgets/option_control.py diff --git a/system/ui/sunnypilot/widgets/progress_bar.py b/openpilot/system/ui/sunnypilot/widgets/progress_bar.py similarity index 100% rename from system/ui/sunnypilot/widgets/progress_bar.py rename to openpilot/system/ui/sunnypilot/widgets/progress_bar.py diff --git a/system/ui/sunnypilot/widgets/sunnylink_pairing_dialog.py b/openpilot/system/ui/sunnypilot/widgets/sunnylink_pairing_dialog.py similarity index 100% rename from system/ui/sunnypilot/widgets/sunnylink_pairing_dialog.py rename to openpilot/system/ui/sunnypilot/widgets/sunnylink_pairing_dialog.py diff --git a/system/ui/sunnypilot/widgets/toggle.py b/openpilot/system/ui/sunnypilot/widgets/toggle.py similarity index 100% rename from system/ui/sunnypilot/widgets/toggle.py rename to openpilot/system/ui/sunnypilot/widgets/toggle.py diff --git a/system/ui/sunnypilot/widgets/tree_dialog.py b/openpilot/system/ui/sunnypilot/widgets/tree_dialog.py similarity index 99% rename from system/ui/sunnypilot/widgets/tree_dialog.py rename to openpilot/system/ui/sunnypilot/widgets/tree_dialog.py index 69233b803d..97150e1028 100644 --- a/system/ui/sunnypilot/widgets/tree_dialog.py +++ b/openpilot/system/ui/sunnypilot/widgets/tree_dialog.py @@ -48,9 +48,9 @@ class TreeItemWidget(Button): self.border_radius = 10 self.is_expanded = is_expanded - def _render(self, rect): + def _render(self, _): indent = 60 * self.indent_level - self._rect = rl.Rectangle(rect.x + indent, rect.y, rect.width - indent, rect.height) + self._rect = rl.Rectangle(_.x + indent, _.y, _.width - indent, _.height) if self.is_pressed: color = BUTTON_PRESSED_BACKGROUND_COLORS[self._button_style] elif self.selected and self.ref != "search_bar": diff --git a/system/ui/text.py b/openpilot/system/ui/text.py similarity index 98% rename from system/ui/text.py rename to openpilot/system/ui/text.py index 17e8a507cb..f6bcf4eddf 100755 --- a/system/ui/text.py +++ b/openpilot/system/ui/text.py @@ -2,7 +2,7 @@ import re import sys import pyray as rl -from openpilot.system.hardware import HARDWARE, PC +from openpilot.common.hardware import HARDWARE, PC from openpilot.system.ui.lib.application import BIG_UI, gui_app from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.text_measure import measure_text_cached diff --git a/system/ui/tici_reset.py b/openpilot/system/ui/tici_reset.py similarity index 89% rename from system/ui/tici_reset.py rename to openpilot/system/ui/tici_reset.py index a6603d547e..5c175dbfa6 100755 --- a/system/ui/tici_reset.py +++ b/openpilot/system/ui/tici_reset.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -import os +import subprocess import sys import threading import time @@ -7,7 +7,7 @@ from enum import IntEnum import pyray as rl -from openpilot.system.hardware import PC +from openpilot.common.hardware import PC from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle @@ -37,19 +37,23 @@ class Reset(Widget): self._reset_state = ResetState.NONE self._cancel_button = Button("Cancel", gui_app.request_close) self._confirm_button = Button("Confirm", self._confirm, button_style=ButtonStyle.PRIMARY) - self._reboot_button = Button("Reboot", lambda: os.system("sudo reboot")) + self._reboot_button = Button("Reboot", self._reboot) + + @staticmethod + def _reboot() -> None: + subprocess.run("sudo reboot", shell=True) def _do_erase(self): if PC: return # Removing data and formatting - rm = os.system("sudo rm -rf /data/*") - os.system(f"sudo umount {USERDATA}") - fmt = os.system(f"yes | sudo mkfs.ext4 {USERDATA}") + rm = subprocess.run("sudo rm -rf /data/*", shell=True).returncode + subprocess.run(f"sudo umount {USERDATA}", shell=True) + fmt = subprocess.run(f"yes | sudo mkfs.ext4 {USERDATA}", shell=True).returncode if rm == 0 or fmt == 0: - os.system("sudo reboot") + subprocess.run("sudo reboot", shell=True) else: self._reset_state = ResetState.FAILED diff --git a/system/ui/tici_setup.py b/openpilot/system/ui/tici_setup.py similarity index 99% rename from system/ui/tici_setup.py rename to openpilot/system/ui/tici_setup.py index 9eefb6af53..2b2c1c39f8 100755 --- a/system/ui/tici_setup.py +++ b/openpilot/system/ui/tici_setup.py @@ -10,8 +10,8 @@ from enum import IntEnum import pyray as rl -from cereal import log -from openpilot.system.hardware import HARDWARE +from openpilot.cereal import log +from openpilot.common.hardware import HARDWARE from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.widgets import DialogResult, Widget diff --git a/system/ui/tici_updater.py b/openpilot/system/ui/tici_updater.py similarity index 99% rename from system/ui/tici_updater.py rename to openpilot/system/ui/tici_updater.py index 3a3b0987d0..27cf6579a8 100755 --- a/system/ui/tici_updater.py +++ b/openpilot/system/ui/tici_updater.py @@ -5,7 +5,7 @@ import threading import pyray as rl from enum import IntEnum -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.widgets import Widget diff --git a/system/ui/updater.py b/openpilot/system/ui/updater.py similarity index 100% rename from system/ui/updater.py rename to openpilot/system/ui/updater.py diff --git a/system/ui/widgets/__init__.py b/openpilot/system/ui/widgets/__init__.py similarity index 94% rename from system/ui/widgets/__init__.py rename to openpilot/system/ui/widgets/__init__.py index 4ce1c1b694..f8cb04d8a7 100644 --- a/system/ui/widgets/__init__.py +++ b/openpilot/system/ui/widgets/__init__.py @@ -3,16 +3,25 @@ from __future__ import annotations import abc import pyray as rl from enum import IntEnum -from typing import TypeVar +from typing import Protocol, TypeVar from collections.abc import Callable from openpilot.system.ui.lib.application import gui_app, MousePos, MAX_TOUCH_SLOTS, MouseEvent -try: - from openpilot.selfdrive.ui.ui_state import device -except ImportError: - class Device: - awake = True - device = Device() +class DeviceLike(Protocol): + awake: bool + + +def _get_device() -> DeviceLike: + try: + from openpilot.selfdrive.ui.ui_state import device + return device + except ImportError: + class Device: + awake = True + return Device() + + +device = _get_device() W = TypeVar('W', bound='Widget') @@ -185,16 +194,16 @@ class Widget(abc.ABC): """Optionally update the widget's non-layout state. This is called before rendering.""" @abc.abstractmethod - def _render(self, rect: rl.Rectangle) -> bool | int | None: + def _render(self, rect: rl.Rectangle, /) -> bool | int | None: """Render the widget within the given rectangle.""" def _update_layout_rects(self) -> None: """Optionally update any layout rects on Widget rect change.""" - def _handle_mouse_press(self, mouse_pos: MousePos) -> None: + def _handle_mouse_press(self, mouse_pos: MousePos, /) -> None: """Optionally handle mouse press events.""" - def _handle_mouse_release(self, mouse_pos: MousePos) -> None: + def _handle_mouse_release(self, mouse_pos: MousePos, /) -> None: """Optionally handle mouse release events.""" if self._click_delay is not None: self._click_release_time = rl.get_time() + self._click_delay diff --git a/system/ui/widgets/button.py b/openpilot/system/ui/widgets/button.py similarity index 100% rename from system/ui/widgets/button.py rename to openpilot/system/ui/widgets/button.py diff --git a/system/ui/widgets/confirm_dialog.py b/openpilot/system/ui/widgets/confirm_dialog.py similarity index 100% rename from system/ui/widgets/confirm_dialog.py rename to openpilot/system/ui/widgets/confirm_dialog.py diff --git a/system/ui/widgets/html_render.py b/openpilot/system/ui/widgets/html_render.py similarity index 100% rename from system/ui/widgets/html_render.py rename to openpilot/system/ui/widgets/html_render.py diff --git a/system/ui/widgets/icon_widget.py b/openpilot/system/ui/widgets/icon_widget.py similarity index 100% rename from system/ui/widgets/icon_widget.py rename to openpilot/system/ui/widgets/icon_widget.py diff --git a/system/ui/widgets/inputbox.py b/openpilot/system/ui/widgets/inputbox.py similarity index 100% rename from system/ui/widgets/inputbox.py rename to openpilot/system/ui/widgets/inputbox.py diff --git a/system/ui/widgets/keyboard.py b/openpilot/system/ui/widgets/keyboard.py similarity index 100% rename from system/ui/widgets/keyboard.py rename to openpilot/system/ui/widgets/keyboard.py diff --git a/system/ui/widgets/label.py b/openpilot/system/ui/widgets/label.py similarity index 98% rename from system/ui/widgets/label.py rename to openpilot/system/ui/widgets/label.py index 7fe25ab51d..fdaf6f3148 100644 --- a/system/ui/widgets/label.py +++ b/openpilot/system/ui/widgets/label.py @@ -1,7 +1,6 @@ import math from enum import IntEnum from collections.abc import Callable -from itertools import zip_longest from typing import Union import pyray as rl @@ -188,6 +187,9 @@ class Label(Widget): if self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE text_pos = rl.Vector2(self._rect.x, (self._rect.y + (self._rect.height - total_text_height) // 2)) + elif self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: + total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE + text_pos = rl.Vector2(self._rect.x, self._rect.y + self._rect.height - total_text_height) else: text_pos = rl.Vector2(self._rect.x, self._rect.y) @@ -196,18 +198,18 @@ class Label(Widget): if len(self._text_wrapped) > 0: if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: icon_x = self._rect.x + self._text_padding - text_pos.x = self._icon.width + ICON_PADDING + text_pos.x = self._rect.x + self._icon.width + ICON_PADDING elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: total_width = self._icon.width + ICON_PADDING + text_size.x icon_x = self._rect.x + (self._rect.width - total_width) / 2 - text_pos.x = self._icon.width + ICON_PADDING + text_pos.x = self._rect.x + self._icon.width + ICON_PADDING else: icon_x = (self._rect.x + self._rect.width - text_size.x - self._text_padding) - ICON_PADDING - self._icon.width else: icon_x = self._rect.x + (self._rect.width - self._icon.width) / 2 rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE) - for text, text_size, emojis in zip_longest(self._text_wrapped, self._text_size, self._emojis, fillvalue=[]): + for text, text_size, emojis in zip(self._text_wrapped, self._text_size, self._emojis, strict=True): line_pos = rl.Vector2(text_pos.x, text_pos.y) if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: line_pos.x += self._text_padding diff --git a/system/ui/widgets/layouts.py b/openpilot/system/ui/widgets/layouts.py similarity index 100% rename from system/ui/widgets/layouts.py rename to openpilot/system/ui/widgets/layouts.py diff --git a/system/ui/widgets/list_view.py b/openpilot/system/ui/widgets/list_view.py similarity index 98% rename from system/ui/widgets/list_view.py rename to openpilot/system/ui/widgets/list_view.py index 82613c37c8..08c65208a4 100644 --- a/system/ui/widgets/list_view.py +++ b/openpilot/system/ui/widgets/list_view.py @@ -1,6 +1,6 @@ import os import pyray as rl -from collections.abc import Callable +from collections.abc import Callable, Sequence from abc import ABC from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.multilang import tr @@ -207,7 +207,7 @@ class DualButtonAction(ItemAction): class MultipleButtonAction(ItemAction): - def __init__(self, buttons: list[str | Callable[[], str]], button_width: int, selected_index: int = 0, callback: Callable | None = None): + def __init__(self, buttons: Sequence[str | Callable[[], str]], button_width: int, selected_index: int = 0, callback: Callable | None = None): super().__init__(width=len(buttons) * button_width + (len(buttons) - 1) * RIGHT_ITEM_PADDING, enabled=True) self.buttons = buttons self.button_width = button_width diff --git a/system/ui/widgets/mici_keyboard.py b/openpilot/system/ui/widgets/mici_keyboard.py similarity index 100% rename from system/ui/widgets/mici_keyboard.py rename to openpilot/system/ui/widgets/mici_keyboard.py diff --git a/system/ui/widgets/nav_widget.py b/openpilot/system/ui/widgets/nav_widget.py similarity index 99% rename from system/ui/widgets/nav_widget.py rename to openpilot/system/ui/widgets/nav_widget.py index 11770bbe5d..58ff8123bb 100644 --- a/system/ui/widgets/nav_widget.py +++ b/openpilot/system/ui/widgets/nav_widget.py @@ -78,7 +78,7 @@ class NavWidget(Widget, abc.ABC): # the top of a vertical scroll panel to prevent erroneous swipes return True - def set_back_callback(self, callback: Callable[[], None]) -> None: + def set_back_callback(self, callback: Callable[[], None] | None) -> None: self._back_callback = callback def set_shown_callback(self, callback: Callable[[], None] | None) -> None: diff --git a/system/ui/widgets/network.py b/openpilot/system/ui/widgets/network.py similarity index 97% rename from system/ui/widgets/network.py rename to openpilot/system/ui/widgets/network.py index c2fc427c42..fb999c0441 100644 --- a/system/ui/widgets/network.py +++ b/openpilot/system/ui/widgets/network.py @@ -1,6 +1,6 @@ from enum import IntEnum from functools import partial -from typing import cast +from typing import Any, cast import pyray as rl from openpilot.system.ui.lib.application import gui_app @@ -27,9 +27,9 @@ try: from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.lib.prime_state import PrimeType except Exception: - Params = None - ui_state = None - PrimeType = None + Params: Any = None + ui_state: Any = None + PrimeType: Any = None NM_DEVICE_STATE_NEED_AUTH = 60 MIN_PASSWORD_LENGTH = 8 @@ -111,10 +111,16 @@ class NetworkUI(Widget): class AdvancedNetworkSettings(Widget): def __init__(self, wifi_manager: WifiManager): + # AdvancedNetworkSettings needs the full openpilot environment, standalone apps just use WifiManagerUI + from openpilot.common.params import Params + from openpilot.selfdrive.ui.ui_state import ui_state + from openpilot.selfdrive.ui.lib.prime_state import PrimeType super().__init__() self._wifi_manager = wifi_manager self._wifi_manager.add_callbacks(networks_updated=self._on_network_updated) self._params = Params() + self._prime_state = ui_state.prime_state + self._cell_prime_types = (PrimeType.NONE, PrimeType.LITE) self._keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True) @@ -259,7 +265,7 @@ class AdvancedNetworkSettings(Widget): self._wifi_manager.process_callbacks() # If not using prime SIM, show GSM settings and enable IPv4 forwarding - show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE) + show_cell_settings = self._prime_state.get_type() in self._cell_prime_types self._wifi_manager.set_ipv4_forward(show_cell_settings) self._roaming_btn.set_visible(show_cell_settings) self._apn_btn.set_visible(show_cell_settings) diff --git a/system/ui/widgets/option_dialog.py b/openpilot/system/ui/widgets/option_dialog.py similarity index 100% rename from system/ui/widgets/option_dialog.py rename to openpilot/system/ui/widgets/option_dialog.py diff --git a/system/ui/widgets/scroller.py b/openpilot/system/ui/widgets/scroller.py similarity index 98% rename from system/ui/widgets/scroller.py rename to openpilot/system/ui/widgets/scroller.py index b7b6bf5932..55195b30fb 100644 --- a/system/ui/widgets/scroller.py +++ b/openpilot/system/ui/widgets/scroller.py @@ -1,6 +1,6 @@ import pyray as rl import numpy as np -from collections.abc import Callable +from collections.abc import Callable, Sequence from openpilot.common.filter_simple import FirstOrderFilter, BounceFilter from openpilot.common.swaglog import cloudlog @@ -40,7 +40,7 @@ class ScrollIndicator(Widget): self._content_size = content_size self._viewport = viewport - def _render(self, _): + def _render(self, _, /): # scale indicator width based on content size indicator_w = float(np.interp(self._content_size, [1000, 3000], [300, 100])) @@ -69,7 +69,7 @@ class ScrollIndicator(Widget): class _Scroller(Widget): """Should use wrapper below to reduce boilerplate""" - def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = False, spacing: int = ITEM_SPACING, + def __init__(self, items: Sequence[Widget], horizontal: bool = True, snap_items: bool = False, spacing: int = ITEM_SPACING, pad: int = ITEM_SPACING, scroll_indicator: bool = True, edge_shadows: bool = True): super().__init__() self._items: list[Widget] = [] @@ -150,7 +150,7 @@ class _Scroller(Widget): and not self.moving_items and (original_touch_valid_callback() if original_touch_valid_callback else True)) - def add_widgets(self, items: list[Widget]) -> None: + def add_widgets(self, items: Sequence[Widget]) -> None: for item in items: self.add_widget(item) @@ -332,7 +332,7 @@ class _Scroller(Widget): else: item.render() - def _render(self, _): + def _render(self, _, /): rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height)) @@ -397,7 +397,7 @@ class Scroller(Widget): # pass down enabled to child widget for nav stack self._scroller.set_enabled(lambda: self.enabled) - def _render(self, _): + def _render(self, _, /): self._scroller.render(self._rect) diff --git a/system/ui/widgets/scroller_tici.py b/openpilot/system/ui/widgets/scroller_tici.py similarity index 94% rename from system/ui/widgets/scroller_tici.py rename to openpilot/system/ui/widgets/scroller_tici.py index a843010d56..fc81e1b079 100644 --- a/system/ui/widgets/scroller_tici.py +++ b/openpilot/system/ui/widgets/scroller_tici.py @@ -1,4 +1,5 @@ import pyray as rl +from collections.abc import Sequence from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.widgets import Widget @@ -23,7 +24,7 @@ class LineSeparator(Widget): class Scroller(Widget): - def __init__(self, items: list[Widget], spacing: int = ITEM_SPACING, line_separator: bool = False, pad_end: bool = True): + def __init__(self, items: Sequence[Widget], spacing: int = ITEM_SPACING, line_separator: bool = False, pad_end: bool = True): super().__init__() self._items: list[Widget] = [] self._spacing = spacing diff --git a/system/ui/widgets/slider.py b/openpilot/system/ui/widgets/slider.py similarity index 100% rename from system/ui/widgets/slider.py rename to openpilot/system/ui/widgets/slider.py diff --git a/system/ui/widgets/toggle.py b/openpilot/system/ui/widgets/toggle.py similarity index 100% rename from system/ui/widgets/toggle.py rename to openpilot/system/ui/widgets/toggle.py diff --git a/system/updated/common.py b/openpilot/system/updated/common.py similarity index 100% rename from system/updated/common.py rename to openpilot/system/updated/common.py diff --git a/system/updated/updated.py b/openpilot/system/updated/updated.py similarity index 97% rename from system/updated/updated.py rename to openpilot/system/updated/updated.py index 8e28f7c52d..99696292f1 100755 --- a/system/updated/updated.py +++ b/openpilot/system/updated/updated.py @@ -3,7 +3,6 @@ import os import re import datetime import subprocess -import psutil import shutil import signal import fcntl @@ -17,8 +16,8 @@ from openpilot.common.time_helpers import system_time_valid from openpilot.common.markdown import parse_markdown from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert -from openpilot.system.hardware import AGNOS, HARDWARE -from openpilot.system.version import get_build_metadata, SP_BRANCH_MIGRATIONS +from openpilot.common.hardware import AGNOS, HARDWARE +from openpilot.common.version import get_build_metadata, SP_BRANCH_MIGRATIONS LOCK_FILE = os.getenv("UPDATER_LOCK_FILE", "/tmp/safe_staging_overlay.lock") STAGING_ROOT = os.getenv("UPDATER_STAGING_ROOT", "/data/safe_staging") @@ -194,7 +193,7 @@ def finalize_update() -> None: def handle_agnos_update() -> None: - from openpilot.system.hardware.tici.agnos import flash_agnos_update, get_target_slot_number + from openpilot.common.hardware.tici.agnos import flash_agnos_update, get_target_slot_number cur_version = HARDWARE.get_os_version() updated_version = run(["bash", "-c", r"unset AGNOS_VERSION && source launch_env.sh && \ @@ -210,7 +209,7 @@ def handle_agnos_update() -> None: cloudlog.info(f"Beginning background installation for AGNOS {updated_version}") set_offroad_alert("Offroad_NeosUpdate", True) - manifest_path = os.path.join(OVERLAY_MERGED, "system/hardware/tici/agnos.json") + manifest_path = os.path.join(OVERLAY_MERGED, "openpilot/system/hardware/tici/agnos.json") target_slot_number = get_target_slot_number() flash_agnos_update(manifest_path, target_slot_number, cloudlog) set_offroad_alert("Offroad_NeosUpdate", False) @@ -294,7 +293,7 @@ class Updater: try: branch = self.get_branch(basedir) commit = self.get_commit_hash(basedir)[:7] - with open(os.path.join(basedir, "sunnypilot", "common", "version.h")) as f: + with open(os.path.join(basedir, "openpilot", "sunnypilot", "common", "version.h")) as f: version = f.read().split('"')[1] commit_unix_ts = run(["git", "show", "-s", "--format=%ct", "HEAD"], basedir).rstrip() @@ -343,7 +342,7 @@ class Updater: setup_git_options(OVERLAY_MERGED) output = run(["git", "ls-remote", "--heads"], OVERLAY_MERGED) - self.branches = defaultdict(lambda: None) + self.branches.clear() for line in output.split('\n'): ls_remotes_re = r'(?P\b[0-9a-f]{5,40}\b)(\s+)(refs\/heads\/)(?P.*$)' x = re.fullmatch(ls_remotes_re, line.strip()) @@ -412,11 +411,6 @@ def main() -> None: except OSError as e: raise RuntimeError("couldn't get overlay lock; is another instance running?") from e - # Set low io priority - proc = psutil.Process() - if psutil.LINUX: - proc.ionice(psutil.IOPRIO_CLASS_BE, value=7) - # Check if we just performed an update if Path(os.path.join(STAGING_ROOT, "old_openpilot")).is_dir(): cloudlog.event("update installed") diff --git a/system/sensord/tests/__init__.py b/openpilot/system/webrtc/__init__.py similarity index 100% rename from system/sensord/tests/__init__.py rename to openpilot/system/webrtc/__init__.py diff --git a/system/webrtc/device/video.py b/openpilot/system/webrtc/device/video.py similarity index 57% rename from system/webrtc/device/video.py rename to openpilot/system/webrtc/device/video.py index 8a07c5b9be..c300f76485 100644 --- a/system/webrtc/device/video.py +++ b/openpilot/system/webrtc/device/video.py @@ -1,12 +1,17 @@ import asyncio +from dataclasses import dataclass import struct import time -import av from teleoprtc.tracks import TiciVideoStreamTrack -from cereal import messaging -from openpilot.common.realtime import DT_MDL, DT_DMON +from openpilot.cereal import messaging +from openpilot.common.realtime import DT_MDL +from openpilot.common.params import Params + + +# v4l2 buffer flag marking an encoded keyframe (linux/videodev2.h) +V4L2_BUF_FLAG_KEYFRAME = 0x8 # arbitrary 16-byte UUID identifying openpilot frame-timing SEI messages TIMING_SEI_UUID = bytes([ @@ -16,6 +21,15 @@ TIMING_SEI_UUID = bytes([ _SEI_PREFIX = b'\x00\x00\x00\x01\x06\x05\x30' + TIMING_SEI_UUID +@dataclass(frozen=True) +class EncodedVideoFrame: + data: bytes + pts: int + + def __bytes__(self) -> bytes: + return self.data + + class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): camera_to_sock_mapping = { "driver": "livestreamDriverEncodeData", @@ -23,14 +37,20 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): "road": "livestreamRoadEncodeData", } - def __init__(self, camera_type: str): - dt = DT_DMON if camera_type == "driver" else DT_MDL - super().__init__(camera_type, dt) + def __init__(self, camera_type: str, video_enabled: bool = True): + super().__init__(camera_type, DT_MDL) self._sock = self._make_sock(camera_type) self._pts = 0 self._t0_ns = time.monotonic_ns() self.timing_sei_enabled = False + self.params = Params() + self._seen_keyframe = False + self.video_enabled = video_enabled + + def stop(self) -> None: + super().stop() + self._sock = None def _make_sock(self, camera_type: str) -> messaging.SubSocket: return messaging.sub_sock(self.camera_to_sock_mapping[camera_type], conflate=True) @@ -38,6 +58,14 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): def switch_camera(self, camera_type: str) -> None: self._sock = self._make_sock(camera_type) + def enable(self, enabled: bool): + self.video_enabled = enabled + if not enabled: + self._seen_keyframe = False + + def request_keyframe(self) -> None: + self.params.put("LivestreamRequestKeyframe", True, block=False) + def _build_frame_data(self, msg) -> bytes: encode_data = getattr(msg, msg.which()) if not self.timing_sei_enabled: @@ -54,19 +82,20 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): async def recv(self): while True: + # while video is disabled, pause here without returning + if not self.video_enabled: + await asyncio.sleep(0.005) + continue + msg = messaging.recv_one_or_none(self._sock) if msg is not None: + if not self._seen_keyframe and (getattr(msg, msg.which()).idx.flags & V4L2_BUF_FLAG_KEYFRAME): + self._seen_keyframe = True + self.params.put("LivestreamRequestKeyframe", False, block=False) break await asyncio.sleep(0.005) - packet = av.Packet(self._build_frame_data(msg)) - packet.time_base = self._time_base - self._pts = ((time.monotonic_ns() - self._t0_ns) * self._clock_rate) // 1_000_000_000 - packet.pts = self._pts self.log_debug("track sending frame %d", self._pts) - return packet - - def codec_preference(self) -> str | None: - return "H264" + return EncodedVideoFrame(self._build_frame_data(msg), self._pts) diff --git a/openpilot/system/webrtc/helpers.py b/openpilot/system/webrtc/helpers.py new file mode 100644 index 0000000000..776b64573e --- /dev/null +++ b/openpilot/system/webrtc/helpers.py @@ -0,0 +1,40 @@ +import time +import requests +from dataclasses import asdict, dataclass, field + + +WEBRTCD_PORT = 5001 + +@dataclass +class StreamRequestBody: + sdp: str + init_camera: str + enabled: bool + bridge_services_in: list[str] = field(default_factory=list) + bridge_services_out: list[str] = field(default_factory=list) + + +def post_stream_request(body: StreamRequestBody) -> dict: + t_start = time.monotonic() + try: + resp = requests.post(f"http://localhost:{WEBRTCD_PORT}/stream", json=asdict(body), timeout=10) + t_end = time.monotonic() + ret = resp.json() + ret["time"] = (t_end - t_start) * 1000 + return ret + except requests.ConnectTimeout as e: + raise Exception("webrtc took too long to respond.") from e + except requests.ConnectionError as e: + raise Exception("webrtc server on device is not running.") from e + + +def wait_for_webrtcd(max_retries: float = 10) -> None: + attempts = 0 + while attempts < max_retries: + try: + if requests.get(f"http://localhost:{WEBRTCD_PORT}/schema", timeout=1).ok: + return + except requests.ConnectionError: + attempts += 1 + time.sleep(0.5) + raise TimeoutError("webrtcd did not initialize in time.") diff --git a/system/webrtc/schema.py b/openpilot/system/webrtc/schema.py similarity index 100% rename from system/webrtc/schema.py rename to openpilot/system/webrtc/schema.py diff --git a/system/webrtc/tests/test_stream_session.py b/openpilot/system/webrtc/tests/test_stream_session.py similarity index 74% rename from system/webrtc/tests/test_stream_session.py rename to openpilot/system/webrtc/tests/test_stream_session.py index f44d217d58..1f7bd5747a 100644 --- a/system/webrtc/tests/test_stream_session.py +++ b/openpilot/system/webrtc/tests/test_stream_session.py @@ -1,15 +1,10 @@ import asyncio import json import time -# for aiortc and its dependencies -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) -warnings.filterwarnings("ignore", category=RuntimeWarning) # TODO: remove this when google-crc32c publish a python3.12 wheel -from aiortc import RTCDataChannel -from aiortc.mediastreams import VIDEO_CLOCK_RATE, VIDEO_TIME_BASE import capnp -from cereal import messaging, log +from openpilot.cereal import messaging, log +from teleoprtc.tracks import VIDEO_CLOCK_RATE from openpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIncomingMessageProxy from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack @@ -31,13 +26,13 @@ class TestStreamSession: expected_dict = {"type": "customReservedRawData0", "logMonoTime": 123, "valid": True, "data": "test"} expected_json = json.dumps(expected_dict).encode() - channel = mocker.Mock(spec=RTCDataChannel) - mocked_submaster = messaging.SubMaster(["customReservedRawData0"]) + channel = mocker.Mock() + channel.is_open.return_value = True + proxy = CerealOutgoingMessageProxy(["customReservedRawData0"]) def mocked_update(t): - mocked_submaster.update_msgs(0, [test_msg]) + proxy.sm.update_msgs(0, [test_msg]) mocker.patch.object(messaging.SubMaster, "update", side_effect=mocked_update) - proxy = CerealOutgoingMessageProxy(mocked_submaster) proxy.add_channel(channel) proxy.update() @@ -60,9 +55,11 @@ class TestStreamSession: mocked_pubmaster.send.assert_called_once() mt, md = mocked_pubmaster.send.call_args.args - assert mt == msg["type"] + msg_type = msg["type"] + assert isinstance(msg_type, str) + assert mt == msg_type assert isinstance(md, capnp._DynamicStructBuilder) - assert hasattr(md, msg["type"]) + assert hasattr(md, msg_type) mocked_pubmaster.reset_mock() @@ -74,14 +71,11 @@ class TestStreamSession: track = LiveStreamVideoStreamTrack("driver") assert track.id.startswith("driver") - assert track.codec_preference() == "H264" for i in range(5): packet = self.loop.run_until_complete(track.recv()) - assert packet.time_base == VIDEO_TIME_BASE if i == 0: start_ns = time.monotonic_ns() start_pts = packet.pts assert abs(i + packet.pts - (start_pts + (((time.monotonic_ns() - start_ns) * VIDEO_CLOCK_RATE) // 1_000_000_000))) < 450 #5ms - assert packet.size == 0 - + assert bytes(packet) == b"" diff --git a/openpilot/system/webrtc/webrtcd.py b/openpilot/system/webrtc/webrtcd.py new file mode 100755 index 0000000000..7901f9847f --- /dev/null +++ b/openpilot/system/webrtc/webrtcd.py @@ -0,0 +1,599 @@ +#!/usr/bin/env python3 + +from abc import abstractmethod +from collections.abc import Callable +import os +import socket +import time +import capnp +import argparse +import asyncio +import contextlib +import json +import uuid +import logging +import signal +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import urlparse, parse_qs +from typing import Any + +from openpilot.system.webrtc.helpers import StreamRequestBody +from openpilot.system.webrtc.schema import generate_field +from openpilot.common.params import Params +from openpilot.cereal import messaging, log + + +# socket trick: route lookup for 8.8.8.8 (nothing is sent or actually connected to) +# return the source interfaces IP which is the default interface of the device +def _default_route_ip() -> str | None: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(("8.8.8.8", 53)) # selects a route, sends nothing + return s.getsockname()[0] + except OSError: + return None + finally: + s.close() + +class AsyncTaskRunner: + def __init__(self): + self.task = None + self.logger = logging.getLogger("webrtcd") + + def start(self): + assert self.task is None + self.task = asyncio.create_task(self.run()) + + async def stop(self): + if self.task is None: + return + task = self.task + self.task = None + if task.done(): + return + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task + + @abstractmethod + async def run(self): + pass + + +class CerealOutgoingMessageProxy(AsyncTaskRunner): + def __init__(self, services: list[str], enabled: bool = True): + super().__init__() + self.services = list(services) + self.sm = messaging.SubMaster(self.services) + self.channels = [] + self._enabled = enabled + + def add_channel(self, channel): + self.channels.append(channel) + + def enable(self, enable: bool): + self._enabled = enable + + def to_json(self, msg_content: Any): + if isinstance(msg_content, capnp._DynamicStructReader): + msg_dict = msg_content.to_dict() + elif isinstance(msg_content, capnp._DynamicListReader): + msg_dict = [self.to_json(msg) for msg in msg_content] + elif isinstance(msg_content, bytes): + msg_dict = msg_content.decode() + else: + msg_dict = msg_content + + return msg_dict + + def update(self): + # this is blocking in async context... + self.sm.update(0) + for service, updated in self.sm.updated.items(): + if not updated: + continue + msg_dict = self.to_json(self.sm[service]) + mono_time, valid = self.sm.logMonoTime[service], self.sm.valid[service] + outgoing_msg = {"type": service, "logMonoTime": mono_time, "valid": valid, "data": msg_dict} + encoded_msg = json.dumps(outgoing_msg).encode() + for channel in self.channels: + if not channel.is_open(): + continue + channel.send(encoded_msg) + + async def run(self): + while True: + if not self._enabled: + await asyncio.sleep(0.01) + continue + try: + self.update() + except Exception: + self.logger.exception("Cereal outgoing proxy failure") + await asyncio.sleep(0.01) + + +class CerealIncomingMessageProxy: + def __init__(self, pm: messaging.PubMaster): + self.pm = pm + + def send(self, message: bytes): + msg_json = json.loads(message) + msg_type, msg_data = msg_json["type"], msg_json["data"] + size = None + if not isinstance(msg_data, dict): + size = len(msg_data) + + msg = messaging.new_message(msg_type, size=size) + setattr(msg, msg_type, msg_data) + self.pm.send(msg_type, msg) + + +class DynamicPubMaster(messaging.PubMaster): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.lock = asyncio.Lock() + + async def add_services_if_needed(self, services): + async with self.lock: + for service in services: + if service not in self.sock: + self.sock[service] = messaging.pub_sock(service) + + +class LivestreamBitrateController(AsyncTaskRunner): + bitrates = [500_000, 1_500_000, int(os.environ.get("STREAM_BITRATE", 5_000_000))] + label_to_bitrate = { "high": bitrates[2], "med": bitrates[1], "low": bitrates[0]} + sample_interval = 0.2 + high_level = 0.1 # drop immediately + med_level = 0.05 # drop after # of samples + low_level = 0 # raise after # of samples + down_samples = 5 + param_name = "LivestreamEncoderBitrate" + + def __init__(self, get_stats: Callable[[], dict[str, Any]], params: Params, enabled: bool = True): + super().__init__() + self.get_stats = get_stats + self.params = params + + self.level = 2 + self._publish(self.bitrates[self.level]) + self.prev_stats: tuple[Any, ...] | None = None + self.counter = 0 + self.up_samples = 5 # 1s + self._auto = True + self._enabled = enabled + + def enable(self, enable: bool): + self._enabled = enable + + async def run(self): + while True: + await asyncio.sleep(self.sample_interval) + if not self._enabled: + continue + if not self._auto: + continue + + loss_rate = self._sample() + if loss_rate is None: + continue + if loss_rate >= self.med_level and self.level > 0: + self.counter += 1 + if self.counter >= self.down_samples or loss_rate >= self.high_level: + self.level -= 1 + self.up_samples *= 2 # exponential backoff before raising again + self.counter = 0 + self._publish(self.bitrates[self.level]) + elif loss_rate <= self.low_level and self.level < len(self.bitrates) - 1: + self.counter -= 1 + if -self.counter >= self.up_samples: + self.level += 1 + self.counter = 0 + self._publish(self.bitrates[self.level]) + + def _sample(self) -> float | None: + report = next(iter(self.get_stats().values()), None) + if report is None: + return None + + current = (report.ssrc, report.fraction_lost, report.packets_lost, report.highest_seq_no, report.jitter, report.lsr, report.dlsr) + if self.prev_stats == current: + return None + self.prev_stats = current + + loss_rate = report.fraction_lost / 256 + return loss_rate + + def _publish(self, bitrate: float): + self.params.put(self.param_name, bitrate) + + def set_quality(self, quality): + if quality in self.label_to_bitrate: + self._publish(self.label_to_bitrate[quality]) + self._auto = False + elif quality == "auto": + self._auto = True + + +class StreamSession: + shared_pub_master = DynamicPubMaster([]) + + def __init__(self, body: StreamRequestBody, debug_mode: bool = False): + from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack + from teleoprtc.builder import WebRTCAnswerBuilder + + self.identifier = str(uuid.uuid4()) + self.params = Params() + builder = WebRTCAnswerBuilder(body.sdp, bind_address=_default_route_ip()) + + self.enabled = body.enabled + self.video_track = LiveStreamVideoStreamTrack(body.init_camera, self.enabled) + builder.add_video_stream(body.init_camera, self.video_track) + self.stream = builder.stream() + + self.incoming_bridge: CerealIncomingMessageProxy | None = None + self.incoming_bridge_services = body.bridge_services_in + self.outgoing_bridge: CerealOutgoingMessageProxy | None = None + self.bitrate_controller: LivestreamBitrateController | None = None + if len(body.bridge_services_in) > 0: + self.incoming_bridge = CerealIncomingMessageProxy(self.shared_pub_master) + if len(body.bridge_services_out) > 0: + self.outgoing_bridge = CerealOutgoingMessageProxy(body.bridge_services_out, self.enabled) + self.bitrate_controller = LivestreamBitrateController(self.stream.get_receiver_report_stats, self.params, self.enabled) + + self.run_task: asyncio.Task | None = None + self._cleanup_lock = asyncio.Lock() + self._cleanup_done = False + self.logger = logging.getLogger("webrtcd") + self.logger.info( + "New stream session (%s), init camera %s, video enabled %s, incoming services %s, outgoing services %s", + self.identifier, body.init_camera, body.enabled, body.bridge_services_in, body.bridge_services_out, + ) + + def start(self): + self.run_task = asyncio.create_task(self.run()) + + async def stop(self): + if self.run_task is not None and not self.run_task.done() and self.run_task is not asyncio.current_task(): + self.run_task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await self.run_task + self.run_task = None + await self.post_run_cleanup() + + async def get_answer(self): + return await self.stream.start() + + def message_handler(self, message: bytes): + try: + payload = json.loads(message) if isinstance(message, (bytes, str)) else None + if isinstance(payload, dict): + msg_type = payload.get("type") + + match msg_type: + case "livestreamCameraSwitch": + self.video_track.switch_camera(payload["data"]["camera"]) + case "livestreamSettings": + if self.bitrate_controller is not None: + self.bitrate_controller.set_quality(payload["data"]["quality"]) + case "livestreamVideoEnable": + enabled = payload["data"]["enabled"] + self.enabled = enabled + self.video_track.enable(enabled) + if self.outgoing_bridge is not None: + self.outgoing_bridge.enable(enabled) + if self.bitrate_controller is not None: + self.bitrate_controller.enable(enabled) + if not enabled: + self.params.put("LivestreamRequestKeyframe", True) + case "clockSync": + pong = json.dumps({"type": "clockSync", "data": { + "action": "pong", "browserSendTime": payload["data"]["browserSendTime"], "deviceTime": time.time() * 1000, # noqa: TID251 + }}) + self.stream.get_messaging_channel().send(pong) + case "enableTimingSei": + if hasattr(self.video_track, 'timing_sei_enabled'): + self.video_track.timing_sei_enabled = bool(payload["data"]["enabled"]) + case _: + if payload.get("type") not in self.incoming_bridge_services: + return + if self.incoming_bridge is not None: + self.incoming_bridge.send(message) + except Exception: + self.logger.exception("Cereal incoming proxy failure") + + async def run(self): + try: + self.params.put("LivestreamRequestKeyframe", True) + await asyncio.wait_for(self.stream.wait_for_connection(), timeout=15) + if self.stream.has_messaging_channel(): + self.stream.set_message_handler(self.message_handler) + if self.incoming_bridge is not None: + await self.shared_pub_master.add_services_if_needed(self.incoming_bridge_services) + if self.outgoing_bridge is not None: + channel = self.stream.get_messaging_channel() + self.outgoing_bridge.add_channel(channel) + self.outgoing_bridge.start() + if self.bitrate_controller is not None: + self.bitrate_controller.start() + + self.logger.info("Stream session (%s) connected", self.identifier) + await self.stream.wait_for_disconnection() + self.logger.info("Stream session (%s) ended", self.identifier) + except Exception: + self.logger.exception("Stream session failure") + finally: + await self.post_run_cleanup() + + async def post_run_cleanup(self): + async with self._cleanup_lock: + if self._cleanup_done: + return + self._cleanup_done = True + self.params.put("LivestreamRequestKeyframe", False) + if self.bitrate_controller is not None: + await self.bitrate_controller.stop() + if self.outgoing_bridge is not None: + await self.outgoing_bridge.stop() + if self.video_track is not None: + self.video_track.stop() + self.video_track = None + await self.stream.stop() + + +class ServerState: + def __init__(self, debug: bool): + self.streams: dict[str, StreamSession] = {} + self.stream_lock = asyncio.Lock() + self.debug = debug + self.teardown: asyncio.TimerHandle | None = None + + +# if nothing connects for 5 seconds, tear down livestreaming processes +def schedule_teardown(state: ServerState): + if state.teardown is not None: + state.teardown.cancel() + + def clear(): + if not state.streams: + Params().put_bool("IsLiveStreaming", False) + + state.teardown = asyncio.get_running_loop().call_later(5.0, clear) + + +def _json_response(obj: Any, status: int = 200) -> tuple[int, bytes, str]: + return (status, json.dumps(obj).encode(), "application/json; charset=utf-8") + + +def _text_response(text: str, status: int = 200) -> tuple[int, bytes, str]: + return (status, text.encode(), "text/plain; charset=utf-8") + + +async def handle_get_stream(state: ServerState, raw_body: bytes) -> tuple[int, bytes, str]: + stream_dict, debug_mode = state.streams, state.debug + body = StreamRequestBody(**json.loads(raw_body)) + + async with state.stream_lock: + # don't remove existing connection on prewarm request + enabled = any(s.run_task and not s.run_task.done() and s.enabled for s in stream_dict.values()) + if enabled and not body.enabled: + return _json_response({"error": "busy", "message": "someone else is connected."}) + + for sid, s in list(stream_dict.items()): + if s.run_task and not s.run_task.done(): + try: + ch = s.stream.get_messaging_channel() + ch.send(json.dumps({"type": "disconnect", "data": "Another device has connected, closing this session."})) + except Exception: + pass + await s.stop() + stream_dict.pop(sid, None) + + session = StreamSession(body, debug_mode) + stream_dict[session.identifier] = session + try: + answer = await asyncio.wait_for(session.get_answer(), timeout=30) + except TimeoutError: + await session.stop() + stream_dict.pop(session.identifier, None) + logging.getLogger("webrtcd").exception("Timed out creating stream answer") + raise + except Exception: + await session.stop() + stream_dict.pop(session.identifier, None) + logging.getLogger("webrtcd").exception("Failed to create stream answer") + raise + session.start() + + def remove_finished_session(_: asyncio.Task) -> None: + stream_dict.pop(session.identifier, None) + schedule_teardown(state) + + session.run_task.add_done_callback(remove_finished_session) + + return _json_response({"sdp": answer.sdp, "type": answer.type}) + + +async def handle_get_schema(state: ServerState, services_param: str) -> tuple[int, bytes, str]: + services = services_param.split(",") + services = [s for s in services if s] + assert all(s in log.Event.schema.fields and not s.endswith("DEPRECATED") for s in services), "Invalid service name" + schema_dict = {s: generate_field(log.Event.schema.fields[s]) for s in services} + return _json_response(schema_dict) + + +async def handle_post_notify(state: ServerState, payload: Any) -> tuple[int, bytes, str]: + for session in list(state.streams.values()): + try: + ch = session.stream.get_messaging_channel() + ch.send(json.dumps(payload)) + except Exception: + continue + + return _text_response("OK") + + +async def on_shutdown(state: ServerState): + for session in list(state.streams.values()): + try: + ch = session.stream.get_messaging_channel() + ch.send(json.dumps({"type": "disconnect", "data": "device streaming has been stopped."})) + except Exception: + pass + await session.stop() + state.streams.clear() + + +class WebrtcdHandler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + # path -> allowed methods (aiohttp registered POST /stream, POST /notify, GET /schema + its auto HEAD) + _routes = { + "/schema": ("GET", "HEAD"), + "/stream": ("POST",), + "/notify": ("POST",), + } + + def _send(self, status: int, body: bytes, content_type: str) -> None: + self.send_response(status) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(body))) + self.end_headers() + if self.command != "HEAD": + self.wfile.write(body) + + def _read_body(self) -> bytes: + length = int(self.headers.get("Content-Length", 0)) + return self.rfile.read(length) if length else b"" + + def _run(self, coro) -> tuple[int, bytes, str]: + return asyncio.run_coroutine_threadsafe(coro, self.server.loop).result() + + def _dispatch_request(self) -> None: + parsed = urlparse(self.path) + allowed = self._routes.get(parsed.path) + + try: + if allowed is None: + result = _json_response({"error": "not found"}, status=404) + elif self.command not in allowed: + result = _json_response({"error": "method not allowed"}, status=405) + elif parsed.path == "/schema": + services = parse_qs(parsed.query).get("services", [""])[0] + result = self._run(handle_get_schema(self.server.state, services)) + elif parsed.path == "/stream": + result = self._run(handle_get_stream(self.server.state, self._read_body())) + else: # /notify + try: + payload = json.loads(self._read_body()) + except Exception: + result = _json_response({"error": "bad request"}, status=400) + else: + result = self._run(handle_post_notify(self.server.state, payload)) + except Exception as e: + logging.getLogger("webrtcd").exception("Unhandled error handling %s", self.path) + result = _json_response({"error": "exception", "message": f"{type(e).__name__}: {e}"}, status=500) + + self._send(*result) + + def do_GET(self) -> None: + self._dispatch_request() + + def do_HEAD(self) -> None: + self._dispatch_request() + + def do_POST(self) -> None: + self._dispatch_request() + + def do_PUT(self) -> None: + self._dispatch_request() + + def do_DELETE(self) -> None: + self._dispatch_request() + + def do_PATCH(self) -> None: + self._dispatch_request() + + def do_OPTIONS(self) -> None: + self._dispatch_request() + + def log_message(self, format: str, *args: object) -> None: # noqa: A002 # stdlib override + # silence default access logging; errors are logged explicitly in _dispatch_request + pass + + +class WebrtcdHTTPServer(ThreadingHTTPServer): + daemon_threads = True + allow_reuse_address = True + state: ServerState + loop: asyncio.AbstractEventLoop + + +async def _shutdown(server: WebrtcdHTTPServer, state: ServerState, loop: asyncio.AbstractEventLoop) -> None: + # stop accepting new HTTP connections (blocks until serve_forever returns, so + # run it off the loop) then tear down active stream sessions. + await loop.run_in_executor(None, server.shutdown) + await on_shutdown(state) + loop.stop() + + +def prewarm_stream_session_imports(debug_mode: bool = False) -> None: + from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack + from teleoprtc.builder import WebRTCAnswerBuilder + assert LiveStreamVideoStreamTrack + assert WebRTCAnswerBuilder + + +def webrtcd_thread(host: str, port: int, debug: bool): + logging.basicConfig(level=logging.CRITICAL, handlers=[logging.StreamHandler()]) + prewarm_start = time.monotonic() + prewarm_stream_session_imports(debug) + prewarm_end = time.monotonic() + logging.getLogger("webrtcd").info(f"webrtc prewarm finished in {(prewarm_end - prewarm_start) * 1000} ms") + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + state = ServerState(debug) + + server = WebrtcdHTTPServer((host, port), WebrtcdHandler) + server.state = state + server.loop = loop + + # serve HTTP on a daemon thread so the asyncio loop can own the main thread + http_thread = threading.Thread(target=server.serve_forever, name="webrtcd-http", daemon=True) + http_thread.start() + + shutting_down = False + shutdown_task = None + + def request_shutdown() -> None: + nonlocal shutting_down, shutdown_task + if shutting_down: + return + shutting_down = True + shutdown_task = loop.create_task(_shutdown(server, state, loop)) + + for sig in (signal.SIGINT, signal.SIGTERM): + loop.add_signal_handler(sig, request_shutdown) + + try: + loop.run_forever() + finally: + server.server_close() + loop.close() + + +def main(): + parser = argparse.ArgumentParser(description="WebRTC daemon") + parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to listen on") + parser.add_argument("--port", type=int, default=5001, help="Port to listen on") + parser.add_argument("--debug", action="store_true", help="Enable debug mode") + args = parser.parse_args() + + webrtcd_thread(args.host, args.port, args.debug) + + +if __name__=="__main__": + main() diff --git a/third_party/copyparty/copyparty-sfx.py b/openpilot/third_party/copyparty/copyparty-sfx.py similarity index 100% rename from third_party/copyparty/copyparty-sfx.py rename to openpilot/third_party/copyparty/copyparty-sfx.py diff --git a/third_party/mapd_pfeiferj/README.md b/openpilot/third_party/mapd_pfeiferj/README.md similarity index 100% rename from third_party/mapd_pfeiferj/README.md rename to openpilot/third_party/mapd_pfeiferj/README.md diff --git a/third_party/mapd_pfeiferj/mapd b/openpilot/third_party/mapd_pfeiferj/mapd similarity index 100% rename from third_party/mapd_pfeiferj/mapd rename to openpilot/third_party/mapd_pfeiferj/mapd diff --git a/openpilot/tools b/openpilot/tools deleted file mode 120000 index 4887d6e0c9..0000000000 --- a/openpilot/tools +++ /dev/null @@ -1 +0,0 @@ -../tools \ No newline at end of file diff --git a/system/tests/__init__.py b/openpilot/tools/__init__.py similarity index 100% rename from system/tests/__init__.py rename to openpilot/tools/__init__.py diff --git a/tools/auto_source.py b/openpilot/tools/auto_source.py similarity index 100% rename from tools/auto_source.py rename to openpilot/tools/auto_source.py diff --git a/tools/cabana/.gitignore b/openpilot/tools/cabana/.gitignore similarity index 71% rename from tools/cabana/.gitignore rename to openpilot/tools/cabana/.gitignore index 927b05e34a..7f9ac0fde0 100644 --- a/tools/cabana/.gitignore +++ b/openpilot/tools/cabana/.gitignore @@ -3,7 +3,9 @@ moc_* *.generated.qrc assets.cc +bootstrap_icons.cc _cabana dbc/car_fingerprint_to_dbc.json tests/test_cabana +tests/test_dbc_core diff --git a/tools/cabana/README.md b/openpilot/tools/cabana/README.md similarity index 98% rename from tools/cabana/README.md rename to openpilot/tools/cabana/README.md index a721b1aa13..b30c29640e 100644 --- a/tools/cabana/README.md +++ b/openpilot/tools/cabana/README.md @@ -63,8 +63,8 @@ cabana "5beb9b58bd12b691/0000010a--a51155e496" --dcam --ecam [SSH into your device](https://github.com/commaai/openpilot/wiki/SSH) and start the bridge with the following command: ```shell -cd /data/openpilot/cereal/messaging/ -./bridge & +cd /data/openpilot +./openpilot/cereal/messaging/bridge & ``` Then Run Cabana with the device's IP address: diff --git a/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript similarity index 57% rename from tools/cabana/SConscript rename to openpilot/tools/cabana/SConscript index 04389a9ebf..fcaa3b7930 100644 --- a/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -5,7 +5,7 @@ import shutil import bootstrap_icons import libusb -Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'replay_lib') +Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'replay_lib', 'ffmpeg_libs') # Detect Qt - skip build if not available if arch == "Darwin": @@ -30,7 +30,7 @@ if arch == "Darwin": ] qt_dirs += [f"{qt_env['QTDIR']}/include/Qt{m}" for m in qt_modules] qt_env["LINKFLAGS"] += ["-F" + os.path.join(qt_env['QTDIR'], "lib")] - qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules] + ["OpenGL"] + qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules] qt_env.AppendENVPath('PATH', os.path.join(qt_env['QTDIR'], "bin")) else: qt_install_prefix = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_PREFIX'], encoding='utf8').strip() @@ -46,7 +46,7 @@ else: qt_dirs += [f"{qt_install_headers}/QtGui/{qt_gui_dirs[0]}/QtGui", ] if qt_gui_dirs else [] qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules] - qt_libs = [f"Qt5{m}" for m in qt_modules] + ["GL"] + qt_libs = [f"Qt5{m}" for m in qt_modules] qt_env['QT3DIR'] = qt_env['QTDIR'] qt_env.Tool('qt3') @@ -60,60 +60,73 @@ qt_flags = [ "-DQT_MESSAGELOGCONTEXT", ] qt_env['CXXFLAGS'] += qt_flags -qt_env['LIBPATH'] += ['#selfdrive/ui', ] +qt_env['LIBPATH'] += ['#openpilot/selfdrive/ui', ] qt_env['LIBS'] = qt_libs base_frameworks = qt_env['FRAMEWORKS'] base_libs = [common, messaging, cereal, visionipc, 'm', 'pthread'] + qt_env["LIBS"] if arch == "Darwin": - base_frameworks += ['QtCharts', 'CoreFoundation', 'CoreVideo', 'CoreMedia', 'IOKit', 'Security', 'VideoToolbox'] -else: - base_libs.append('Qt5Charts') + base_frameworks += ['CoreFoundation', 'CoreVideo', 'CoreMedia', 'IOKit', 'Security', 'VideoToolbox'] cabana_env = qt_env.Clone() cabana_env['CPPPATH'] += [libusb.INCLUDE_DIR] cabana_env['LIBPATH'] += [libusb.LIB_DIR] -cabana_libs = [cereal, messaging, visionipc, replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'bz2', 'zstd', 'yuv', 'usb-1.0'] + base_libs -if arch != "Darwin": - cabana_libs += ['va', 'va-drm', 'drm'] -opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc/dbc").abspath) +cabana_libs = [cereal, messaging, visionipc, replay_lib] + ffmpeg_libs + ['bz2', 'zstd', 'usb-1.0'] + base_libs +opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../../opendbc_repo/opendbc/dbc").abspath) cabana_env['CXXFLAGS'] += [opendbc_path] -def write_assets_qrc(target, source, env): - with open(str(source[0])) as f: - qrc = f.read() - with open(str(target[0]), "w") as f: - f.write(qrc.replace("@BOOTSTRAP_ICONS_SVG@", str(bootstrap_icons.SVG_PATH))) +# embed the bootstrap icons SVG into the binary +def build_bootstrap_icons_src(target, source, env): + data = open(str(source[0]), 'rb').read() + with open(str(target[0]), 'w') as f: + f.write('#include \n') + f.write('extern const unsigned char bootstrap_icons_svg[];\n') + f.write('extern const size_t bootstrap_icons_svg_len;\n') + f.write('const unsigned char bootstrap_icons_svg[] = {\n') + for i in range(0, len(data), 32): + f.write(','.join(str(b) for b in data[i:i+32]) + ',\n') + f.write('};\n') + f.write('const size_t bootstrap_icons_svg_len = sizeof(bootstrap_icons_svg);\n') + return None + +bootstrap_icons_src = cabana_env.Command('assets/bootstrap_icons.cc', str(bootstrap_icons.SVG_PATH), build_bootstrap_icons_src) # build assets assets = "assets/assets.cc" -assets_src = cabana_env.Command( - "assets/assets.generated.qrc", - "assets/assets.qrc", - write_assets_qrc, -) -cabana_env.Command(assets, assets_src, f"rcc $SOURCES -o $TARGET") -cabana_env.Depends(assets_src, str(bootstrap_icons.SVG_PATH)) -cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "assets/assets.o"])) +cabana_env.Command(assets, "assets/assets.qrc", f"rcc $SOURCES -o $TARGET") +cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"])) cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', - 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', + 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'dbc/dbcqt.cc', 'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'] if arch != "Darwin": cabana_srcs += ['streams/socketcanstream.cc'] -cabana_lib = cabana_env.Library("cabana_lib", cabana_srcs, LIBS=cabana_libs, FRAMEWORKS=base_frameworks) +cabana_lib = cabana_env.Library("cabana_lib", cabana_srcs + [bootstrap_icons_src], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) cabana_env.Program('_cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) if GetOption('extras'): cabana_env.Program('tests/test_cabana', ['tests/test_runner.cc', 'tests/test_cabana.cc', cabana_lib], LIBS=[cabana_libs]) -output_json_file = 'tools/cabana/dbc/car_fingerprint_to_dbc.json' + # This target deliberately uses the base environment and links no Qt libraries. + # It prevents Qt dependencies from creeping back into the DBC core. + dbc_core_test_env = env.Clone() + dbc_core_test_env['CXXFLAGS'] += [opendbc_path] + dbc_core_test_objects = [ + dbc_core_test_env.Object('tests/dbc_core_test_runner', 'tests/test_runner.cc'), + dbc_core_test_env.Object('tests/dbc_core_tests', 'tests/test_cabana.cc'), + dbc_core_test_env.Object('tests/dbc_core_model', 'dbc/dbc.cc'), + dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'), + dbc_core_test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'), + ] + dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects) + +output_json_file = 'openpilot/tools/cabana/dbc/car_fingerprint_to_dbc.json' generate_dbc = cabana_env.Command('#' + output_json_file, ['dbc/generate_dbc_json.py'], - "python3 tools/cabana/dbc/generate_dbc_json.py --out " + output_json_file) -cabana_env.Depends(generate_dbc, ["#common", '#opendbc_repo', "#cereal", "#msgq_repo"]) + "python3 openpilot/tools/cabana/dbc/generate_dbc_json.py --out " + output_json_file) +cabana_env.Depends(generate_dbc, ["#openpilot/common", '#opendbc_repo', "#openpilot/cereal", "#msgq_repo"]) diff --git a/tools/cabana/assets/assets.qrc b/openpilot/tools/cabana/assets/assets.qrc similarity index 59% rename from tools/cabana/assets/assets.qrc rename to openpilot/tools/cabana/assets/assets.qrc index f5880e5580..009d63f008 100644 --- a/tools/cabana/assets/assets.qrc +++ b/openpilot/tools/cabana/assets/assets.qrc @@ -1,6 +1,5 @@ - @BOOTSTRAP_ICONS_SVG@ cabana-icon.png diff --git a/tools/cabana/assets/cabana-icon.png b/openpilot/tools/cabana/assets/cabana-icon.png similarity index 100% rename from tools/cabana/assets/cabana-icon.png rename to openpilot/tools/cabana/assets/cabana-icon.png diff --git a/tools/cabana/binaryview.cc b/openpilot/tools/cabana/binaryview.cc similarity index 93% rename from tools/cabana/binaryview.cc rename to openpilot/tools/cabana/binaryview.cc index c86b3ebdae..5e919dc6a3 100644 --- a/tools/cabana/binaryview.cc +++ b/openpilot/tools/cabana/binaryview.cc @@ -1,8 +1,10 @@ #include "tools/cabana/binaryview.h" +#include "tools/cabana/dbc/dbcqt.h" #include -#include +#include + #include #include #include @@ -34,8 +36,8 @@ BinaryView::BinaryView(QWidget *parent) : QTableView(parent) { setMouseTracking(true); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &BinaryView::refresh); - QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &BinaryView::refresh); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &BinaryView::refresh); + QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &BinaryView::refresh); addShortcuts(); setWhatsThis(R"( @@ -64,7 +66,7 @@ void BinaryView::addShortcuts() { QObject::connect(shortcut_delete_backspace, &QShortcut::activated, shortcut_delete_x, &QShortcut::activated); QObject::connect(shortcut_delete_x, &QShortcut::activated, [=]{ if (hovered_sig != nullptr) { - UndoStack::push(new RemoveSigCommand(model->msg_id, hovered_sig)); + UndoStack::instance()->push(new RemoveSigCommand(model->msg_id, hovered_sig)); hovered_sig = nullptr; } }); @@ -124,7 +126,7 @@ void BinaryView::highlight(const cabana::Signal *sig) { } void BinaryView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags) { - auto index = indexAt(viewport()->mapFromGlobal(QCursor::pos())); + auto index = indexAt(last_mouse_pos); if (!anchor_index.isValid() || !index.isValid()) return; @@ -139,7 +141,7 @@ void BinaryView::setSelection(const QRect &rect, QItemSelectionModel::SelectionF void BinaryView::mousePressEvent(QMouseEvent *event) { resize_sig = nullptr; - if (auto index = indexAt(event->pos()); index.isValid() && index.column() != 8) { + if (auto index = indexAt(last_mouse_pos = event->pos()); index.isValid() && index.column() != 8) { anchor_index = index; auto item = (const BinaryViewModel::Item *)anchor_index.internalPointer(); int bit_pos = get_bit_pos(anchor_index); @@ -156,7 +158,7 @@ void BinaryView::mousePressEvent(QMouseEvent *event) { } void BinaryView::highlightPosition(const QPoint &pos) { - if (auto index = indexAt(viewport()->mapFromGlobal(pos)); index.isValid()) { + if (auto index = indexAt(pos); index.isValid()) { auto item = (BinaryViewModel::Item *)index.internalPointer(); const cabana::Signal *sig = item->sigs.empty() ? nullptr : item->sigs.back(); highlight(sig); @@ -164,7 +166,7 @@ void BinaryView::highlightPosition(const QPoint &pos) { } void BinaryView::mouseMoveEvent(QMouseEvent *event) { - highlightPosition(event->globalPos()); + highlightPosition(last_mouse_pos = event->pos()); QTableView::mouseMoveEvent(event); } @@ -177,7 +179,7 @@ void BinaryView::mouseReleaseEvent(QMouseEvent *event) { auto sig = resize_sig ? *resize_sig : cabana::Signal{}; std::tie(sig.start_bit, sig.size, sig.is_little_endian) = getSelection(release_index); resize_sig ? emit editSignal(resize_sig, sig) - : UndoStack::push(new AddSigCommand(model->msg_id, sig)); + : UndoStack::instance()->push(new AddSigCommand(model->msg_id, sig)); } else { auto item = (const BinaryViewModel::Item *)anchor_index.internalPointer(); if (item && item->sigs.size() > 0) @@ -206,7 +208,7 @@ void BinaryView::refresh() { resize_sig = nullptr; hovered_sig = nullptr; model->refresh(); - highlightPosition(QCursor::pos()); + if (underMouse()) highlightPosition(last_mouse_pos); } std::set BinaryView::getOverlappingSignals() const { @@ -259,7 +261,8 @@ void BinaryViewModel::refresh() { int pos = sig->is_little_endian ? flipBitPos(sig->start_bit + j) : flipBitPos(sig->start_bit) + j; int idx = column_count * (pos / 8) + pos % 8; if (idx >= items.size()) { - qWarning() << "signal " << sig->name.c_str() << "out of bounds.start_bit:" << sig->start_bit << "size:" << sig->size; + fprintf(stderr, "signal %s out of bounds.start_bit: %d size: %d\n", + sig->name.c_str(), sig->start_bit, sig->size); break; } if (j == 0) sig->is_little_endian ? items[idx].is_lsb = true : items[idx].is_msb = true; @@ -334,7 +337,7 @@ void BinaryViewModel::updateState() { color.setAlpha(alpha); updateItem(i, j, bit_val, color); } - updateItem(i, 8, binary[i], last_msg.colors[i]); + updateItem(i, 8, binary[i], toQColor(last_msg.colors[i])); } } @@ -421,14 +424,14 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op painter->fillRect(option.rect, item->bg_color); } } else if (option.state & QStyle::State_Selected) { - auto color = bin_view->resize_sig ? bin_view->resize_sig->color : option.palette.color(QPalette::Active, QPalette::Highlight); + auto color = bin_view->resize_sig ? toQColor(bin_view->resize_sig->color) : option.palette.color(QPalette::Active, QPalette::Highlight); painter->fillRect(option.rect, color); painter->setPen(option.palette.color(QPalette::BrightText)); } else if (!bin_view->selectionModel()->hasSelection() || std::find(item->sigs.begin(), item->sigs.end(), bin_view->resize_sig) == item->sigs.end()) { // not resizing if (item->sigs.size() > 0) { for (auto &s : item->sigs) { if (s == bin_view->hovered_sig) { - painter->fillRect(option.rect, s->color.darker(125)); // 4/5x brightness + painter->fillRect(option.rect, toQColor(s->color.darker(125))); // 4/5x brightness } else { drawSignalCell(painter, option, index, s); } @@ -483,14 +486,14 @@ void BinaryItemDelegate::drawSignalCell(QPainter *painter, const QStyleOptionVie painter->setClipRegion(QRegion(rc).subtracted(subtract)); auto item = (const BinaryViewModel::Item *)index.internalPointer(); - QColor color = sig->color; + QColor color = toQColor(sig->color); color.setAlpha(item->bg_color.alpha()); // Mixing the signal color with the Base background color to fade it painter->fillRect(rc, option.palette.color(QPalette::Base)); painter->fillRect(rc, color); // Draw edges - color = sig->color.darker(125); + color = toQColor(sig->color.darker(125)); painter->setPen(QPen(color, 1)); if (draw_left) painter->drawLine(rc.topLeft(), rc.bottomLeft()); if (draw_right) painter->drawLine(rc.topRight(), rc.bottomRight()); diff --git a/tools/cabana/binaryview.h b/openpilot/tools/cabana/binaryview.h similarity index 99% rename from tools/cabana/binaryview.h rename to openpilot/tools/cabana/binaryview.h index e568228b37..c49067a1a2 100644 --- a/tools/cabana/binaryview.h +++ b/openpilot/tools/cabana/binaryview.h @@ -94,6 +94,7 @@ private: void highlightPosition(const QPoint &pt); QModelIndex anchor_index; + QPoint last_mouse_pos{-1, -1}; BinaryViewModel *model; BinaryItemDelegate *delegate; bool is_message_active = false; diff --git a/tools/cabana/cabana b/openpilot/tools/cabana/cabana similarity index 86% rename from tools/cabana/cabana rename to openpilot/tools/cabana/cabana index 5eb15eabf0..db613b5391 100755 --- a/tools/cabana/cabana +++ b/openpilot/tools/cabana/cabana @@ -2,7 +2,7 @@ set -e DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -ROOT="$(cd "$DIR/../../" && pwd)" +ROOT="$(cd "$DIR/../../../" && pwd)" install_qt() { if [[ "$(uname)" == "Darwin" ]]; then @@ -33,6 +33,6 @@ fi # Build _cabana cd "$ROOT" -scons tools/cabana/_cabana cereal/messaging/bridge +scons -u openpilot/tools/cabana/_cabana openpilot/cereal/messaging/bridge exec "$DIR/_cabana" "$@" diff --git a/openpilot/tools/cabana/cabana.cc b/openpilot/tools/cabana/cabana.cc new file mode 100644 index 0000000000..d8e21815dc --- /dev/null +++ b/openpilot/tools/cabana/cabana.cc @@ -0,0 +1,187 @@ +#include +#include +#include +#include + +#include + +#include "tools/cabana/mainwin.h" +#include "tools/cabana/streams/devicestream.h" +#include "tools/cabana/streams/pandastream.h" +#include "tools/cabana/streams/replaystream.h" +#ifdef __linux__ +#include "tools/cabana/streams/socketcanstream.h" +#endif + +namespace { + +struct CabanaArgs { + bool demo = false; + bool auto_source = false; + bool qcam = false; + bool ecam = false; + bool dcam = false; + bool msgq = false; + bool panda = false; + bool no_vipc = false; + std::string panda_serial; + std::string socketcan; + std::string zmq; + std::string data_dir; + std::string dbc; + std::string route; +}; + +void printUsage(const char *argv0) { + fprintf(stderr, + "Usage: %s [options] [route]\n" + "\n" + " route the drive to replay. find your drives at connect.comma.ai\n" + "\n" + "Options:\n" + " --help show this help\n" + " --demo use a demo route instead of providing your own\n" + " --auto Auto load the route from the best available source (no video):\n" + " internal, openpilotci, comma_api, car_segments, testing_closet\n" + " --qcam load qcamera\n" + " --ecam load wide road camera\n" + " --dcam load driver camera\n" + " --msgq read can messages from the msgq\n" + " --panda read can messages from panda\n" + " --panda-serial read can messages from panda with given serial\n" +#ifdef __linux__ + " --socketcan read can messages from given SocketCAN device\n" +#endif + " --zmq read can messages from zmq at the specified ip-address\n" + " --data_dir local directory with routes\n" + " --no-vipc do not output video\n" + " --dbc dbc file to open\n", + argv0); +} + +// Returns true if value was consumed from argv[i+1]. +bool takeValue(int argc, char *argv[], int &i, std::string &out) { + if (i + 1 >= argc) { + fprintf(stderr, "error: %s requires a value\n", argv[i]); + return false; + } + out = argv[++i]; + return true; +} + +// Returns 0 to continue, or a process exit code (0 for --help, 1 for errors). +int parseArgs(int argc, char *argv[], CabanaArgs &args, bool &ok) { + ok = false; + for (int i = 1; i < argc; ++i) { + const char *a = argv[i]; + if (std::strcmp(a, "--help") == 0 || std::strcmp(a, "-h") == 0) { + printUsage(argv[0]); + return 0; + } else if (std::strcmp(a, "--demo") == 0) { + args.demo = true; + } else if (std::strcmp(a, "--auto") == 0) { + args.auto_source = true; + } else if (std::strcmp(a, "--qcam") == 0) { + args.qcam = true; + } else if (std::strcmp(a, "--ecam") == 0) { + args.ecam = true; + } else if (std::strcmp(a, "--dcam") == 0) { + args.dcam = true; + } else if (std::strcmp(a, "--msgq") == 0) { + args.msgq = true; + } else if (std::strcmp(a, "--panda") == 0) { + args.panda = true; + } else if (std::strcmp(a, "--panda-serial") == 0) { + if (!takeValue(argc, argv, i, args.panda_serial)) return 1; + args.panda = true; + } else if (std::strcmp(a, "--socketcan") == 0) { + if (!takeValue(argc, argv, i, args.socketcan)) return 1; +#ifdef __linux__ +#else + fprintf(stderr, "error: --socketcan is only supported on Linux\n"); + return 1; +#endif + } else if (std::strcmp(a, "--zmq") == 0) { + if (!takeValue(argc, argv, i, args.zmq)) return 1; + } else if (std::strcmp(a, "--data_dir") == 0) { + if (!takeValue(argc, argv, i, args.data_dir)) return 1; + } else if (std::strcmp(a, "--no-vipc") == 0) { + args.no_vipc = true; + } else if (std::strcmp(a, "--dbc") == 0) { + if (!takeValue(argc, argv, i, args.dbc)) return 1; + } else if (a[0] == '-') { + fprintf(stderr, "error: unknown option %s\n", a); + printUsage(argv[0]); + return 1; + } else if (args.route.empty()) { + args.route = a; + } else { + fprintf(stderr, "error: unexpected argument %s\n", a); + printUsage(argv[0]); + return 1; + } + } + ok = true; + return 0; +} + +} // namespace + +int main(int argc, char *argv[]) { + QCoreApplication::setApplicationName("Cabana"); + initApp(argc, argv, false); + QApplication app(argc, argv); + app.setApplicationDisplayName("Cabana"); + //app.setWindowIcon(QIcon(":cabana-icon.png")); // TODO: do this in imgui + + UnixSignalHandler signalHandler; + utils::setTheme(settings.theme); + + CabanaArgs args; + bool args_ok = false; + if (const int code = parseArgs(argc, argv, args, args_ok); !args_ok) { + return code; + } + + AbstractStream *stream = nullptr; + + if (args.msgq) { + stream = new DeviceStream(&app); + } else if (!args.zmq.empty()) { + stream = new DeviceStream(&app, QString::fromStdString(args.zmq)); + } else if (args.panda || !args.panda_serial.empty()) { + try { + stream = new PandaStream(&app, {.serial = args.panda_serial}); + } catch (std::exception &e) { + fprintf(stderr, "%s\n", e.what()); + return 0; + } +#ifdef __linux__ + } else if (SocketCanStream::available() && !args.socketcan.empty()) { + stream = new SocketCanStream(&app, {.device = args.socketcan}); +#endif + } else { + uint32_t replay_flags = REPLAY_FLAG_NONE; + if (args.ecam) replay_flags |= REPLAY_FLAG_ECAM; + if (args.qcam) replay_flags |= REPLAY_FLAG_QCAMERA; + if (args.dcam) replay_flags |= REPLAY_FLAG_DCAM; + if (args.no_vipc) replay_flags |= REPLAY_FLAG_NO_VIPC; + + QString route; + if (!args.route.empty()) { + route = QString::fromStdString(args.route); + } else if (args.demo) { + route = DEMO_ROUTE; + } + if (!route.isEmpty()) { + auto replay_stream = std::make_unique(&app); + if (!replay_stream->loadRoute(route.toStdString(), args.data_dir, replay_flags, args.auto_source)) { + return 0; + } + stream = replay_stream.release(); + } + } + + MainWindow w(stream, QString::fromStdString(args.dbc)); + return app.exec(); +} diff --git a/openpilot/tools/cabana/cameraview.cc b/openpilot/tools/cabana/cameraview.cc new file mode 100644 index 0000000000..a8df373dc4 --- /dev/null +++ b/openpilot/tools/cabana/cameraview.cc @@ -0,0 +1,125 @@ +#include "tools/cabana/cameraview.h" + +#include +#include +#include +#include + +#include +#include + +#include "common/yuv.h" + +CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, QWidget* parent) : + stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QWidget(parent) { + setAttribute(Qt::WA_OpaquePaintEvent); + qRegisterMetaType>("availableStreams"); + QObject::connect(this, &CameraWidget::vipcThreadFrameReceived, this, &CameraWidget::vipcFrameReceived, Qt::QueuedConnection); + QObject::connect(this, &CameraWidget::vipcAvailableStreamsUpdated, this, &CameraWidget::availableStreamsUpdated, Qt::QueuedConnection); + QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit, this, &CameraWidget::stopVipcThread); +} + +CameraWidget::~CameraWidget() { + stopVipcThread(); +} + +void CameraWidget::showEvent(QShowEvent *event) { + if (!vipc_thread.joinable()) { + clearFrames(); + vipc_exit = false; + vipc_thread = std::thread(&CameraWidget::vipcThread, this); + } +} + +void CameraWidget::stopVipcThread() { + vipc_exit = true; + if (vipc_thread.joinable()) { + vipc_thread.join(); + } +} + +void CameraWidget::availableStreamsUpdated(std::set streams) { + available_streams = streams; +} + +void CameraWidget::paintEvent(QPaintEvent *event) { + QPainter p(this); + p.fillRect(rect(), bg); + + std::lock_guard lk(frame_lock); + if (rgb_frame.isNull()) return; + + // Scale for aspect ratio + float widget_ratio = (float)width() / height(); + float frame_ratio = (float)rgb_frame.width() / rgb_frame.height(); + int w = std::lround(width() * std::min(frame_ratio / widget_ratio, 1.0f)); + int h = std::lround(height() * std::min(widget_ratio / frame_ratio, 1.0f)); + QRect video_rect((width() - w) / 2, (height() - h) / 2, w, h); + + p.setRenderHint(QPainter::SmoothPixmapTransform); + if (active_stream_type == VISION_STREAM_DRIVER) { + // mirror driver camera horizontally + const qreal cx = video_rect.x() + video_rect.width() / 2.0; + p.translate(cx, 0); + p.scale(-1, 1); + p.translate(-cx, 0); + } + p.drawImage(video_rect, rgb_frame); +} + +void CameraWidget::vipcFrameReceived() { + update(); +} + +void CameraWidget::vipcThread() { + VisionStreamType cur_stream = requested_stream_type; + std::unique_ptr vipc_client; + VisionIpcBufExtra frame_meta = {}; + + while (!vipc_exit) { + if (!vipc_client || cur_stream != requested_stream_type) { + clearFrames(); + fprintf(stderr, "connecting to stream %d, was connected to %d\n", + (int)requested_stream_type, (int)cur_stream); + cur_stream = requested_stream_type; + vipc_client.reset(new VisionIpcClient(stream_name, cur_stream, false)); + } + active_stream_type = cur_stream; + + if (!vipc_client->connected) { + clearFrames(); + auto streams = VisionIpcClient::getAvailableStreams(stream_name, false); + if (streams.empty()) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + emit vipcAvailableStreamsUpdated(streams); + + if (!vipc_client->connect(false)) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + } + + if (VisionBuf *buf = vipc_client->recv(&frame_meta, 100)) { + // NV12 -> RGBA once per frame on the receive thread; paint just draws the image + if (rgb_back.width() != (int)buf->width || rgb_back.height() != (int)buf->height) { + rgb_back = QImage(buf->width, buf->height, QImage::Format_RGBA8888); + } + yuv::nv12_to_rgba(buf->y, buf->stride, buf->uv, buf->stride, + rgb_back.bits(), rgb_back.bytesPerLine(), buf->width, buf->height); + { + std::lock_guard lk(frame_lock); + rgb_frame.swap(rgb_back); + } + emit vipcThreadFrameReceived(); + } + } +} + +void CameraWidget::clearFrames() { + std::lock_guard lk(frame_lock); + rgb_frame = QImage(); + rgb_back = QImage(); + available_streams.clear(); +} diff --git a/tools/cabana/cameraview.h b/openpilot/tools/cabana/cameraview.h similarity index 60% rename from tools/cabana/cameraview.h rename to openpilot/tools/cabana/cameraview.h index 930b13d82c..40d3776005 100644 --- a/tools/cabana/cameraview.h +++ b/openpilot/tools/cabana/cameraview.h @@ -1,23 +1,21 @@ #pragma once -#include +#include #include #include #include +#include #include -#include -#include -#include -#include +#include +#include #include "msgq/visionipc/visionipc_client.h" -class CameraWidget : public QOpenGLWidget, protected QOpenGLFunctions { +class CameraWidget : public QWidget { Q_OBJECT public: - using QOpenGLWidget::QOpenGLWidget; explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, QWidget* parent = nullptr); ~CameraWidget(); void setStreamType(VisionStreamType type) { requested_stream_type = type; } @@ -26,37 +24,30 @@ public: signals: void clicked(); - void vipcThreadConnected(VisionIpcClient *); void vipcThreadFrameReceived(); void vipcAvailableStreamsUpdated(std::set); protected: - void paintGL() override; - void initializeGL() override; + void paintEvent(QPaintEvent *event) override; void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override { stopVipcThread(); } void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); } void vipcThread(); void clearFrames(); - GLuint frame_vao, frame_vbo, frame_ibo; - GLuint textures[2]; - std::unique_ptr shader_program_; QColor bg = Qt::black; + QImage rgb_frame; // written by vipc thread, drawn by GUI thread; guarded by frame_lock + QImage rgb_back; // vipc thread only std::string stream_name; - int stream_width = 0; - int stream_height = 0; - int stream_stride = 0; std::atomic active_stream_type; std::atomic requested_stream_type; std::set available_streams; - QThread *vipc_thread = nullptr; - std::recursive_mutex frame_lock; - VisionBuf* current_frame_ = nullptr; - VisionIpcBufExtra frame_meta_ = {}; + std::thread vipc_thread; + std::atomic vipc_exit = false; + std::mutex frame_lock; protected slots: - void vipcConnected(VisionIpcClient *vipc_client); void vipcFrameReceived(); void availableStreamsUpdated(std::set streams); }; diff --git a/openpilot/tools/cabana/chart/chart.cc b/openpilot/tools/cabana/chart/chart.cc new file mode 100644 index 0000000000..8496c26994 --- /dev/null +++ b/openpilot/tools/cabana/chart/chart.cc @@ -0,0 +1,770 @@ +#include "tools/cabana/chart/chart.h" +#include "tools/cabana/dbc/dbcqt.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include "tools/cabana/chart/chartswidget.h" + +const int AXIS_X_TOP_MARGIN = 4; +const int X_TICK_COUNT = 5; +const double MIN_ZOOM_SECONDS = 0.01; // 10ms +// Define a small value of epsilon to compare double values +const float EPSILON = 0.000001; +static inline bool xLessThan(const QPointF &p, float x) { return p.x() < (x - EPSILON); } + +static QMargins layoutMargins(const QStyle *style) { + return { + style->pixelMetric(QStyle::PM_LayoutLeftMargin), + style->pixelMetric(QStyle::PM_LayoutTopMargin), + style->pixelMetric(QStyle::PM_LayoutRightMargin), + style->pixelMetric(QStyle::PM_LayoutBottomMargin), + }; +} + +ChartView::ChartView(const std::pair &x_range, ChartsWidget *parent) + : x_min(x_range.first), x_max(x_range.second), charts_widget(parent), QWidget(parent) { + series_type = (SeriesType)settings.chart_series_type; + align_to = 50; + setMouseTracking(true); + tip_label = new TipLabel(this); + createToolButtons(); + signal_value_font.setPointSize(9); + + QObject::connect(dbcNotifier(), &QtDBCNotifier::signalRemoved, this, &ChartView::signalRemoved); + QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &ChartView::signalUpdated); + QObject::connect(dbcNotifier(), &QtDBCNotifier::msgRemoved, this, &ChartView::msgRemoved); + QObject::connect(dbcNotifier(), &QtDBCNotifier::msgUpdated, this, &ChartView::msgUpdated); +} + +void ChartView::createToolButtons() { + close_btn = new ToolButton("x", tr("Remove Chart"), this); + + menu = new QMenu(this); + // series types + auto change_series_group = new QActionGroup(menu); + change_series_group->setExclusive(true); + QStringList types{tr("Line"), tr("Step Line"), tr("Scatter")}; + for (int i = 0; i < types.size(); ++i) { + QAction *act = new QAction(types[i], change_series_group); + act->setData(i); + act->setCheckable(true); + act->setChecked(i == (int)series_type); + menu->addAction(act); + } + menu->addSeparator(); + menu->addAction(tr("Manage Signals"), this, &ChartView::manageSignals); + split_chart_act = menu->addAction(tr("Split Chart"), [this]() { charts_widget->splitChart(this); }); + + manage_btn = new ToolButton("list", "", this); + manage_btn->setMenu(menu); + manage_btn->setPopupMode(QToolButton::InstantPopup); + manage_btn->setStyleSheet("QToolButton::menu-indicator { image: none; }"); + + close_act = new QAction(tr("Close"), this); + QObject::connect(close_act, &QAction::triggered, [this] () { charts_widget->removeChart(this); }); + QObject::connect(close_btn, &QToolButton::clicked, close_act, &QAction::triggered); + QObject::connect(change_series_group, &QActionGroup::triggered, [this](QAction *action) { + setSeriesType((SeriesType)action->data().toInt()); + }); +} + +QSize ChartView::sizeHint() const { + return {CHART_MIN_WIDTH, settings.chart_height}; +} + +void ChartView::addSignal(const MessageId &msg_id, const cabana::Signal *sig) { + if (hasSignal(msg_id, sig)) return; + + sigs.push_back({.msg_id = msg_id, .sig = sig, .color = uniqueColor(toQColor(sig->color))}); + updateSeries(sig); + updateTitle(); + emit charts_widget->seriesChanged(); +} + +bool ChartView::hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const { + return std::any_of(sigs.cbegin(), sigs.cend(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; }); +} + +void ChartView::removeIf(std::function predicate) { + int prev_size = sigs.size(); + sigs.erase(std::remove_if(sigs.begin(), sigs.end(), predicate), sigs.end()); + if (sigs.empty()) { + charts_widget->removeChart(this); + } else if (sigs.size() != prev_size) { + emit charts_widget->seriesChanged(); + updateAxisY(); + updateTitle(); + } +} + +void ChartView::signalUpdated(const cabana::Signal *sig) { + auto it = std::find_if(sigs.begin(), sigs.end(), [sig](auto &s) { return s.sig == sig; }); + if (it != sigs.end()) { + if (it->color != toQColor(sig->color)) { + it->color = uniqueColor(toQColor(sig->color), sig); + } + updateTitle(); + updateSeries(sig); + } +} + +void ChartView::msgUpdated(MessageId id) { + if (std::any_of(sigs.cbegin(), sigs.cend(), [=](auto &s) { return s.msg_id.address == id.address; })) { + updateTitle(); + } +} + +void ChartView::manageSignals() { + SignalSelector dlg(tr("Manage Chart"), this); + for (auto &s : sigs) { + dlg.addSelected(s.msg_id, s.sig); + } + if (dlg.exec() == QDialog::Accepted) { + auto items = dlg.seletedItems(); + for (auto s : items) { + addSignal(s->msg_id, s->sig); + } + removeIf([&](auto &s) { + return std::none_of(items.cbegin(), items.cend(), [&](auto &it) { return s.msg_id == it->msg_id && s.sig == it->sig; }); + }); + } +} + +void ChartView::resizeEvent(QResizeEvent *event) { + QWidget::resizeEvent(event); + const auto margins = layoutMargins(style()); + QPixmap grip = utils::icon("grip-horizontal"); + move_icon_rect = QRect(QPoint(margins.left(), margins.top()), grip.size() / grip.devicePixelRatio()); + close_btn->resize(close_btn->sizeHint()); + manage_btn->resize(manage_btn->sizeHint()); + close_btn->move(rect().right() - margins.right() - close_btn->width(), margins.top()); + manage_btn->move(close_btn->x() - manage_btn->width() - style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing), margins.top()); + updatePlotArea(align_to, true); +} + +void ChartView::updatePlotArea(int left_pos, bool force) { + if (align_to != left_pos || force) { + align_to = left_pos; + + const auto margins = layoutMargins(style()); + QFont bold_font = font(); + bold_font.setBold(true); + QFontMetrics fm(font()), bfm(bold_font); + const int marker_size = fm.height() - 4; + const int row_height = std::max(marker_size, fm.height()) + QFontMetrics(signal_value_font).height() + 3; + const int legend_left = move_icon_rect.right() + margins.left(); + const int legend_right = std::max(manage_btn->x() - margins.right(), legend_left + 10); + + // layout legend entries left-to-right, wrapping between the move icon and the buttons + legend_rects.clear(); + int x = legend_left, y = margins.top(); + for (auto &s : sigs) { + int w = marker_size + 5 + bfm.horizontalAdvance(QString::fromStdString(s.sig->name)) + + fm.horizontalAdvance(QString::fromStdString(" " + msgName(s.msg_id) + " " + s.msg_id.toString())); + w = std::min(w, legend_right - legend_left); // keep oversized entries clear of the header buttons + if (x + w > legend_right && x > legend_left) { + x = legend_left; + y += row_height; + } + legend_rects.emplace_back(x, y, w, std::max(marker_size, fm.height())); + x += w + 12; + } + + // add top space for the legend and signal values + int adjust_top = (y + row_height) - margins.top(); + adjust_top = std::max(adjust_top, manage_btn->geometry().bottom() + style()->pixelMetric(QStyle::PM_LayoutTopMargin)); + // add right space for x-axis label + QSizeF x_label_size = fm.size(Qt::TextSingleLine, QString::number(x_max, 'f', xAxisPrecision())) + QSizeF{5, 5}; + plot_area = rect().adjusted(align_to + margins.left(), adjust_top + margins.top(), + -x_label_size.width() / 2 - margins.right(), + -x_label_size.height() - margins.bottom()); + resetChartCache(); + } +} + +void ChartView::updateTitle() { + split_chart_act->setEnabled(sigs.size() > 1); + updatePlotArea(align_to, true); +} + +void ChartView::updatePlot(double cur, double min, double max) { + cur_sec = cur; + if (min != x_min || max != x_max) { + x_min = min; + x_max = max; + updateAxisY(); + // update tooltip + if (tooltip_x >= 0) { + showTip(secondsAtPoint({tooltip_x, 0})); + } + resetChartCache(); + } + update(); +} + +void ChartView::appendCanEvents(const cabana::Signal *sig, const std::vector &events, + std::vector &vals, std::vector &step_vals) { + vals.reserve(vals.size() + events.capacity()); + step_vals.reserve(step_vals.size() + events.capacity() * 2); + + double value = 0; + for (const CanEvent *e : events) { + if (sig->getValue(e->dat, e->size, &value)) { + const double ts = can->toSeconds(e->mono_time); + vals.emplace_back(ts, value); + if (!step_vals.empty()) + step_vals.emplace_back(ts, step_vals.back().y()); + step_vals.emplace_back(ts, value); + } + } +} + +void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap *msg_new_events) { + for (auto &s : sigs) { + if (!sig || s.sig == sig) { + if (!msg_new_events) { + s.vals.clear(); + s.step_vals.clear(); + } + auto events = msg_new_events ? msg_new_events : &can->eventsMap(); + auto it = events->find(s.msg_id); + if (it == events->end() || it->second.empty()) continue; + + if (s.vals.empty() || can->toSeconds(it->second.back()->mono_time) > s.vals.back().x()) { + appendCanEvents(s.sig, it->second, s.vals, s.step_vals); + } else { + std::vector vals, step_vals; + appendCanEvents(s.sig, it->second, vals, step_vals); + s.vals.insert(std::lower_bound(s.vals.begin(), s.vals.end(), vals.front().x(), xLessThan), + vals.begin(), vals.end()); + s.step_vals.insert(std::lower_bound(s.step_vals.begin(), s.step_vals.end(), step_vals.front().x(), xLessThan), + step_vals.begin(), step_vals.end()); + } + + if (!can->liveStreaming()) { + s.segment_tree.build(s.vals); + } + } + } + updateAxisY(); + // invoke resetChartCache in ui thread + QMetaObject::invokeMethod(this, &ChartView::resetChartCache, Qt::QueuedConnection); +} + +// auto zoom on yaxis +void ChartView::updateAxisY() { + if (sigs.empty()) return; + + double min = std::numeric_limits::max(); + double max = std::numeric_limits::lowest(); + QString unit = QString::fromStdString(sigs[0].sig->unit); + + for (auto &s : sigs) { + if (!s.visible) continue; + + // Only show unit when all signals have the same unit + if (unit != QString::fromStdString(s.sig->unit)) { + unit.clear(); + } + + auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), x_min, xLessThan); + auto last = std::lower_bound(first, s.vals.cend(), x_max, xLessThan); + s.min = std::numeric_limits::max(); + s.max = std::numeric_limits::lowest(); + if (can->liveStreaming()) { + for (auto it = first; it != last; ++it) { + if (it->y() < s.min) s.min = it->y(); + if (it->y() > s.max) s.max = it->y(); + } + } else { + std::tie(s.min, s.max) = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last)); + } + min = std::min(min, s.min); + max = std::max(max, s.max); + } + if (min == std::numeric_limits::max()) min = 0; + if (max == std::numeric_limits::lowest()) max = 0; + + if (y_unit != unit) { + y_unit = unit; + y_label_width = 0; // recalc width + } + + double delta = std::abs(max - min) < 1e-3 ? 1 : (max - min) * 0.05; + auto [min_y, max_y, tick_count] = getNiceAxisNumbers(min - delta, max + delta, 3); + if (min_y != y_min || max_y != y_max || y_label_width == 0) { + y_min = min_y; + y_max = max_y; + y_tick_count = tick_count; + y_precision = std::max(int(-std::floor(std::log10((max_y - min_y) / (tick_count - 1)))), 0); + + QFontMetrics fm(font()); + int max_label_width = 0; + for (int i = 0; i < tick_count; i++) { + qreal value = min_y + (i * (max_y - min_y) / (tick_count - 1)); + max_label_width = std::max(max_label_width, fm.horizontalAdvance(QString::number(value, 'f', y_precision))); + } + + int title_spacing = y_unit.isEmpty() ? 0 : fm.size(Qt::TextSingleLine, y_unit).height(); + y_label_width = title_spacing + max_label_width + 15; + emit axisYLabelWidthChanged(y_label_width); + } +} + +std::tuple ChartView::getNiceAxisNumbers(qreal min, qreal max, int tick_count) { + qreal range = niceNumber((max - min), true); // range with ceiling + qreal step = niceNumber(range / (tick_count - 1), false); + min = std::floor(min / step); + max = std::ceil(max / step); + tick_count = int(max - min) + 1; + return {min * step, max * step, tick_count}; +} + +int ChartView::xAxisPrecision() const { + return std::max(int(-std::floor(std::log10((x_max - x_min) / (X_TICK_COUNT - 1)))), 2); +} + +// nice numbers can be expressed as form of 1*10^n, 2* 10^n or 5*10^n +qreal ChartView::niceNumber(qreal x, bool ceiling) { + qreal z = std::pow(10, std::floor(std::log10(x))); //find corresponding number of the form of 10^n than is smaller than x + qreal q = x / z; //q<10 && q>=1; + if (ceiling) { + if (q <= 1.0) q = 1; + else if (q <= 2.0) q = 2; + else if (q <= 5.0) q = 5; + else q = 10; + } else { + if (q < 1.5) q = 1; + else if (q < 3.0) q = 2; + else if (q < 7.0) q = 5; + else q = 10; + } + return q * z; +} + +void ChartView::contextMenuEvent(QContextMenuEvent *event) { + QMenu context_menu(this); + context_menu.addActions(menu->actions()); + context_menu.addSeparator(); + context_menu.addAction(charts_widget->undo_zoom_action); + context_menu.addAction(charts_widget->redo_zoom_action); + context_menu.addSeparator(); + context_menu.addAction(close_act); + context_menu.exec(event->globalPos()); +} + +void ChartView::mousePressEvent(QMouseEvent *event) { + press_pos = event->pos(); + if (event->button() == Qt::LeftButton && move_icon_rect.contains(event->pos())) { + charts_widget->startChartDrag(this, event->globalPos()); + } else if (event->button() == Qt::LeftButton && event->modifiers().testFlag(Qt::ShiftModifier)) { + // Save current playback state when scrubbing + resume_after_scrub = !can->isPaused(); + if (resume_after_scrub) { + can->pause(true); + } + mouse_mode = MouseMode::Scrub; + } else if (event->button() == Qt::LeftButton && plot_area.contains(event->pos())) { + mouse_mode = MouseMode::Rubber; + rubber_rect = QRect(); + } else { + QWidget::mousePressEvent(event); + } +} + +void ChartView::mouseMoveEvent(QMouseEvent *ev) { + // Scrubbing + if (mouse_mode == MouseMode::Scrub && ev->modifiers().testFlag(Qt::ShiftModifier)) { + if (plot_area.contains(ev->pos())) { + can->seekTo(std::clamp(secondsAtPoint(ev->pos()), can->minSeconds(), can->maxSeconds())); + } + } + + if (mouse_mode == MouseMode::Rubber) { + // horizontal selection, clamped to the plot area + int left = std::clamp(std::min(press_pos.x(), ev->pos().x()), plot_area.left(), plot_area.right()); + int right = std::clamp(std::max(press_pos.x(), ev->pos().x()), plot_area.left(), plot_area.right()); + rubber_rect = QRect(left, plot_area.top(), right - left, plot_area.height()); + update(); + } + + clearTrackPoints(); + if (mouse_mode != MouseMode::Rubber && plot_area.contains(ev->pos()) && isActiveWindow()) { + charts_widget->showValueTip(secondsAtPoint(ev->pos())); + } else if (tip_label->isVisible()) { + charts_widget->showValueTip(-1); + } + QWidget::mouseMoveEvent(ev); +} + +void ChartView::mouseReleaseEvent(QMouseEvent *event) { + if (event->button() == Qt::LeftButton && mouse_mode == MouseMode::Rubber) { + mouse_mode = MouseMode::None; + // Prevent zooming/seeking past the end of the route + double min = std::clamp(secondsAtPoint(rubber_rect.topLeft()), can->minSeconds(), can->maxSeconds()); + double max = std::clamp(secondsAtPoint(rubber_rect.bottomRight()), can->minSeconds(), can->maxSeconds()); + if (rubber_rect.width() <= 0) { + // no rubber dragged, seek to mouse position + can->seekTo(std::clamp(secondsAtPoint(press_pos), can->minSeconds(), can->maxSeconds())); + } else if (rubber_rect.width() > 10 && (max - min) > MIN_ZOOM_SECONDS) { + charts_widget->zoom_undo_stack.push(new ZoomCommand({min, max})); + } + rubber_rect = QRect(); + update(); + } else if (event->button() == Qt::LeftButton && mouse_mode == MouseMode::None && sigs.size() > 1) { + // toggle series visibility by clicking its legend entry + for (int i = 0; i < sigs.size() && i < legend_rects.size(); ++i) { + if (legend_rects[i].contains(press_pos) && legend_rects[i].contains(event->pos())) { + sigs[i].visible = !sigs[i].visible; + updateAxisY(); + updateTitle(); + break; + } + } + } else if (event->button() == Qt::RightButton) { + charts_widget->zoom_undo_stack.undo(); + } else { + QWidget::mouseReleaseEvent(event); + } + + // Resume playback if we were scrubbing + if (mouse_mode == MouseMode::Scrub) { + mouse_mode = MouseMode::None; + if (resume_after_scrub) { + can->pause(false); + resume_after_scrub = false; + } + } +} + +void ChartView::takeSignalsFrom(ChartView *source) { + for (auto &s : source->sigs) { + sigs.push_back(std::move(s)); + sigs.back().color = uniqueColor(sigs.back().color, sigs.back().sig); + } + source->sigs.clear(); + updateAxisY(); + updateTitle(); + charts_widget->removeChart(source); +} + +void ChartView::showTip(double sec) { + QRect tip_area(0, plot_area.top(), rect().width(), plot_area.height()); + QRect visible_rect = charts_widget->chartVisibleRect(this).intersected(tip_area); + if (visible_rect.isEmpty()) { + tip_label->hide(); + return; + } + + tooltip_x = xPos(sec); + qreal x = -1; + QStringList text_list; + for (auto &s : sigs) { + if (s.visible) { + QString value = "--"; + // use reverse iterator to find last item <= sec. + auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double v) { return p.x() > v; }); + if (it != s.vals.crend() && it->x() >= x_min) { + value = QString::fromStdString(s.sig->formatValue(it->y(), false)); + s.track_pt = *it; + x = std::max(x, xPos(it->x())); + } + QString name = sigs.size() > 1 ? QString::fromStdString(s.sig->name) + ": " : ""; + QString min = s.min == std::numeric_limits::max() ? "--" : QString::number(s.min); + QString max = s.max == std::numeric_limits::lowest() ? "--" : QString::number(s.max); + text_list << QString("%2%3 (%4, %5)") + .arg(s.color.name(), name, value, min, max); + } + } + if (x < 0) { + x = tooltip_x; + } + QPoint pt(x, plot_area.top()); + text_list.push_front(QString::number(secondsAtPoint({x, 0}), 'f', 3)); + QString text = "

" % text_list.join("
") % "

"; + tip_label->showText(pt, text, this, visible_rect); + update(); +} + +void ChartView::hideTip() { + clearTrackPoints(); + tooltip_x = -1; + tip_label->hide(); + update(); +} + +void ChartView::resetChartCache() { + chart_pixmap = QPixmap(); + update(); +} + +void ChartView::paintEvent(QPaintEvent *event) { + QPainter painter(this); + painter.setRenderHints(QPainter::Antialiasing); + + // the static layer is invalidated on x-range change and data merge, so cache it in live mode too + const qreal dpr = devicePixelRatioF(); + if (chart_pixmap.isNull() || chart_pixmap.size() != size() * dpr) { + chart_pixmap = QPixmap(size() * dpr); + chart_pixmap.setDevicePixelRatio(dpr); + QPainter p(&chart_pixmap); + p.setRenderHints(QPainter::Antialiasing); + p.setFont(font()); + drawStaticLayer(&p); + } + painter.drawPixmap(QPoint(), chart_pixmap); + + if (can_drop) { + painter.setPen(QPen(palette().color(QPalette::Highlight), 4)); + painter.drawRect(rect()); + } + drawForeground(&painter); +} + +void ChartView::drawStaticLayer(QPainter *painter) { + painter->fillRect(rect(), palette().color(QPalette::Base)); + painter->drawPixmap(move_icon_rect.topLeft(), utils::icon("grip-horizontal")); + drawAxes(painter); + drawLegend(painter); + drawSeries(painter); +} + +void ChartView::drawAxes(QPainter *painter) { + const QColor text_color = palette().color(QPalette::Text); + QColor grid_color = text_color; + grid_color.setAlpha(50); + QFontMetrics fm(font()); + painter->setFont(font()); + + // y grid lines and tick labels + for (int i = 0; i < y_tick_count; ++i) { + double value = y_min + i * (y_max - y_min) / (y_tick_count - 1); + qreal y = yPos(value); + painter->setPen(grid_color); + painter->drawLine(QPointF(plot_area.left(), y), QPointF(plot_area.right(), y)); + painter->setPen(text_color); + QRectF label_rect(0, y - fm.height() / 2.0, plot_area.left() - 6, fm.height()); + painter->drawText(label_rect, Qt::AlignRight | Qt::AlignVCenter, QString::number(value, 'f', y_precision)); + } + + // rotated y axis title (unit) + if (!y_unit.isEmpty()) { + painter->save(); + painter->translate(plot_area.left() - y_label_width + fm.height() / 2.0, plot_area.center().y()); + painter->rotate(-90); + painter->drawText(QRectF(-plot_area.height() / 2.0, -fm.height() / 2.0, plot_area.height(), fm.height()), + Qt::AlignCenter, y_unit); + painter->restore(); + } + + // x grid lines and tick labels + const int x_precision = xAxisPrecision(); + for (int i = 0; i < X_TICK_COUNT; ++i) { + double sec = x_min + i * (x_max - x_min) / (X_TICK_COUNT - 1); + qreal x = xPos(sec); + painter->setPen(grid_color); + painter->drawLine(QPointF(x, plot_area.top()), QPointF(x, plot_area.bottom())); + painter->setPen(text_color); + QString label = QString::number(sec, 'f', x_precision); + QRectF label_rect(x - 100, plot_area.bottom() + AXIS_X_TOP_MARGIN, 200, fm.height()); + painter->drawText(label_rect, Qt::AlignHCenter | Qt::AlignTop, label); + } +} + +void ChartView::drawLegend(QPainter *painter) { + QColor title_color = palette().color(QPalette::WindowText); + // Draw message details in similar color, but slightly fade it to the background + QColor msg_color = title_color; + msg_color.setAlpha(180); + QFont bold_font = font(); + bold_font.setBold(true); + const int marker_size = QFontMetrics(font()).height() - 4; + + for (int i = 0; i < sigs.size() && i < legend_rects.size(); ++i) { + const auto &s = sigs[i]; + const QRect &r = legend_rects[i]; + painter->setPen(Qt::NoPen); + painter->setBrush(s.color); + QRectF marker_rect(r.left(), r.center().y() - marker_size / 2.0, marker_size, marker_size); + series_type == SeriesType::Scatter ? painter->drawEllipse(marker_rect) : painter->drawRect(marker_rect); + + bold_font.setStrikeOut(!s.visible); + QFont normal_font = font(); + normal_font.setStrikeOut(!s.visible); + + qreal x = r.left() + marker_size + 5; + painter->setFont(bold_font); + painter->setPen(title_color); + QString name = QFontMetrics(bold_font).elidedText(QString::fromStdString(s.sig->name), Qt::ElideRight, r.right() - x); + painter->drawText(QRectF(x, r.top(), r.right() - x, r.height()), Qt::AlignLeft | Qt::AlignVCenter, name); + x += QFontMetrics(bold_font).horizontalAdvance(name); + painter->setFont(normal_font); + painter->setPen(msg_color); + QString msg = QFontMetrics(normal_font).elidedText(QString::fromStdString(" " + msgName(s.msg_id) + " " + s.msg_id.toString()), + Qt::ElideRight, r.right() - x); + painter->drawText(QRectF(x, r.top(), r.right() - x, r.height()), Qt::AlignLeft | Qt::AlignVCenter, msg); + } +} + +void ChartView::drawSeries(QPainter *painter) { + painter->save(); + painter->setClipRect(plot_area); + for (auto &s : sigs) { + if (!s.visible) continue; + + // visible points in vals to compute point density + auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), x_min, xLessThan); + auto last = std::lower_bound(first, s.vals.cend(), x_max, xLessThan); + int num_points = std::max(last - first, 1); + double pixels_per_point = 0; + if (first != last) { + const QPointF &right_pt = last == s.vals.cend() ? s.vals.back() : *last; + pixels_per_point = (xPos(right_pt.x()) - xPos(first->x())) / num_points; + } + + if (series_type == SeriesType::Scatter) { + qreal radius = std::clamp(pixels_per_point / 2.0, 2.0, 8.0) / 2.0; + painter->setPen(Qt::NoPen); + painter->setBrush(s.color); + for (auto it = first; it != last; ++it) { + painter->drawEllipse(QPointF(xPos(it->x()), yPos(it->y())), radius, radius); + } + } else { + const auto &points = series_type == SeriesType::StepLine ? s.step_vals : s.vals; + auto begin = std::lower_bound(points.cbegin(), points.cend(), x_min, xLessThan); + if (begin != points.cbegin()) --begin; + auto end = std::lower_bound(begin, points.cend(), x_max, xLessThan); + if (end != points.cend()) ++end; + if (begin == end) continue; + + std::vector polyline; + polyline.reserve(end - begin); + for (auto it = begin; it != end; ++it) { + polyline.emplace_back(xPos(it->x()), yPos(it->y())); + } + painter->setPen(QPen(s.color, 2)); + painter->setBrush(Qt::NoBrush); + painter->drawPolyline(polyline.data(), polyline.size()); + + // show points when zoomed in enough + if (num_points == 1 || pixels_per_point > 20) { + painter->setPen(Qt::NoPen); + painter->setBrush(s.color); + for (auto it = first; it != last; ++it) { + painter->drawEllipse(QPointF(xPos(it->x()), yPos(it->y())), 4, 4); + } + } + } + } + painter->restore(); +} + +void ChartView::drawForeground(QPainter *painter) { + drawTimeline(painter); + drawSignalValue(painter); + // draw track points + painter->setPen(Qt::NoPen); + qreal track_line_x = -1; + for (auto &s : sigs) { + if (!s.track_pt.isNull() && s.visible) { + painter->setBrush(s.color.darker(125)); + QPointF pos(xPos(s.track_pt.x()), yPos(s.track_pt.y())); + painter->drawEllipse(pos, 5.5, 5.5); + track_line_x = std::max(track_line_x, pos.x()); + } + } + if (track_line_x > 0) { + painter->setPen(QPen(Qt::darkGray, 1, Qt::DashLine)); + painter->drawLine(QPointF{track_line_x, (qreal)plot_area.top()}, QPointF{track_line_x, (qreal)plot_area.bottom()}); + } + + drawRubberBandTimeRange(painter); +} + +void ChartView::drawRubberBandTimeRange(QPainter *painter) { + if (rubber_rect.width() <= 1) return; + + // selection rect + QColor highlight = palette().color(QPalette::Highlight); + QColor fill = highlight; + fill.setAlpha(50); + painter->fillRect(rubber_rect, fill); + painter->setPen(highlight); + painter->setBrush(Qt::NoBrush); + painter->drawRect(rubber_rect); + + // time labels at the bottom corners + painter->setPen(Qt::white); + painter->setFont(font()); + for (const auto &pt : {rubber_rect.bottomLeft(), rubber_rect.bottomRight()}) { + QString sec = QString::number(secondsAtPoint(pt), 'f', 2); + auto r = painter->fontMetrics().boundingRect(sec).adjusted(-6, -AXIS_X_TOP_MARGIN, 6, AXIS_X_TOP_MARGIN); + pt == rubber_rect.bottomLeft() ? r.moveTopRight(pt + QPoint{0, 2}) : r.moveTopLeft(pt + QPoint{0, 2}); + painter->fillRect(r, Qt::gray); + painter->drawText(r, Qt::AlignCenter, sec); + } +} + +void ChartView::drawTimeline(QPainter *painter) { + // draw vertical time line + qreal x = std::clamp(xPos(cur_sec), (qreal)plot_area.left(), (qreal)plot_area.right()); + painter->setPen(QPen(palette().color(QPalette::Text), 1)); + painter->drawLine(QPointF{x, plot_area.top() - 1.0}, QPointF{x, plot_area.bottom() + 1.0}); + + // draw current time under the axis-x + QString time_str = QString::number(cur_sec, 'f', 2); + QSize time_str_size = QFontMetrics(font()).size(Qt::TextSingleLine, time_str) + QSize(8, 2); + QRectF time_str_rect(QPointF(x - time_str_size.width() / 2.0, plot_area.bottom() + AXIS_X_TOP_MARGIN), time_str_size); + QPainterPath path; + path.addRoundedRect(time_str_rect, 3, 3); + painter->fillPath(path, utils::isDarkTheme() ? Qt::darkGray : Qt::gray); + painter->setPen(palette().color(QPalette::BrightText)); + painter->setFont(font()); + painter->drawText(time_str_rect, Qt::AlignCenter, time_str); +} + +void ChartView::drawSignalValue(QPainter *painter) { + painter->setFont(signal_value_font); + painter->setPen(palette().color(QPalette::Text)); + for (int i = 0; i < sigs.size() && i < legend_rects.size(); ++i) { + const auto &s = sigs[i]; + auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), cur_sec, + [](auto &p, double x) { return p.x() > x + EPSILON; }); + QString value = (it != s.vals.crend() && it->x() >= x_min) ? QString::fromStdString(s.sig->formatValue(it->y())) : "--"; + QRectF value_rect(legend_rects[i].bottomLeft() - QPoint(0, 1), legend_rects[i].size()); + QString elided_val = painter->fontMetrics().elidedText(value, Qt::ElideRight, value_rect.width()); + painter->drawText(value_rect, Qt::AlignHCenter | Qt::AlignTop, elided_val); + } +} + +QColor ChartView::uniqueColor(QColor color, const cabana::Signal *exclude) const { + for (auto &s : sigs) { + if (s.sig != exclude && std::abs(color.hueF() - s.color.hueF()) < 0.1) { + // use different color to distinguish it from others. + auto last_color = sigs.back().color; + static thread_local std::mt19937 rng{std::random_device{}()}; + std::uniform_int_distribution sat(35, 99); + std::uniform_int_distribution val(85, 99); + color.setHsvF(std::fmod(last_color.hueF() + 60 / 360.0, 1.0), + sat(rng) / 100.0, + val(rng) / 100.0); + break; + } + } + return color; +} + +void ChartView::setSeriesType(SeriesType type) { + if (type != series_type) { + series_type = type; + menu->actions()[(int)type]->setChecked(true); + updateTitle(); + } +} diff --git a/tools/cabana/chart/chart.h b/openpilot/tools/cabana/chart/chart.h similarity index 66% rename from tools/cabana/chart/chart.h rename to openpilot/tools/cabana/chart/chart.h index f9472bd4f6..b03623475a 100644 --- a/tools/cabana/chart/chart.h +++ b/openpilot/tools/cabana/chart/chart.h @@ -1,18 +1,11 @@ #pragma once +#include #include #include #include #include -#include -#include -#include -#include -#include -#include -#include -using namespace QtCharts; #include "tools/cabana/chart/tiplabel.h" #include "tools/cabana/dbc/dbcmanager.h" @@ -25,7 +18,7 @@ enum class SeriesType { }; class ChartsWidget; -class ChartView : public QChartView { +class ChartView : public QWidget { Q_OBJECT public: @@ -38,13 +31,15 @@ public: void updatePlotArea(int left, bool force = false); void showTip(double sec); void hideTip(); - void startAnimation(); - double secondsAtPoint(const QPointF &pt) const { return chart()->mapToValue(pt).x(); } + double secondsAtPoint(const QPointF &pt) const { + return x_min + (pt.x() - plot_area.left()) * (x_max - x_min) / std::max(plot_area.width(), 1); + } struct SigItem { MessageId msg_id; const cabana::Signal *sig = nullptr; - QXYSeries *series = nullptr; + QColor color; + bool visible = true; std::vector vals; std::vector step_vals; QPointF track_pt{}; @@ -59,7 +54,6 @@ signals: private slots: void signalUpdated(const cabana::Signal *sig); void manageSignals(); - void handleMarkerClicked(); void msgUpdated(MessageId id); void msgRemoved(MessageId id) { removeIf([=](auto &s) { return s.msg_id.address == id.address && !dbc()->msg(id); }); } void signalRemoved(const cabana::Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); } @@ -68,52 +62,65 @@ private: void appendCanEvents(const cabana::Signal *sig, const std::vector &events, std::vector &vals, std::vector &step_vals); void createToolButtons(); - void addSeries(QXYSeries *series); void contextMenuEvent(QContextMenuEvent *event) override; void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *ev) override; - void dragEnterEvent(QDragEnterEvent *event) override; - void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator(false); } - void dragMoveEvent(QDragMoveEvent *event) override; - void dropEvent(QDropEvent *event) override; void resizeEvent(QResizeEvent *event) override; QSize sizeHint() const override; void updateAxisY(); void updateTitle(); void resetChartCache(); - void setTheme(QChart::ChartTheme theme); void paintEvent(QPaintEvent *event) override; - void drawForeground(QPainter *painter, const QRectF &rect) override; - void drawBackground(QPainter *painter, const QRectF &rect) override; - void drawDropIndicator(bool draw) { if (std::exchange(can_drop, draw) != can_drop) viewport()->update(); } + void drawStaticLayer(QPainter *painter); + void drawAxes(QPainter *painter); + void drawLegend(QPainter *painter); + void drawSeries(QPainter *painter); + void drawForeground(QPainter *painter); void drawSignalValue(QPainter *painter); void drawTimeline(QPainter *painter); void drawRubberBandTimeRange(QPainter *painter); + int xAxisPrecision() const; std::tuple getNiceAxisNumbers(qreal min, qreal max, int tick_count); qreal niceNumber(qreal x, bool ceiling); - QXYSeries *createSeries(SeriesType type, QColor color); - void setSeriesColor(QXYSeries *, QColor color); - void updateSeriesPoints(); + QColor uniqueColor(QColor color, const cabana::Signal *exclude = nullptr) const; void removeIf(std::function predicate); + void takeSignalsFrom(ChartView *source); + void setDropHighlight(bool highlight) { if (std::exchange(can_drop, highlight) != highlight) update(); } inline void clearTrackPoints() { for (auto &s : sigs) s.track_pt = {}; } + inline qreal xPos(double sec) const { return plot_area.left() + (sec - x_min) / (x_max - x_min) * plot_area.width(); } + inline qreal yPos(double val) const { return plot_area.bottom() - (val - y_min) / (y_max - y_min) * plot_area.height(); } + // layout + QRect plot_area; + QRect move_icon_rect; + std::vector legend_rects; + // axes + double x_min; + double x_max; + double y_min = 0; + double y_max = 1; + int y_tick_count = 3; + int y_precision = 0; + QString y_unit; int y_label_width = 0; int align_to = 0; - QValueAxis *axis_x; - QValueAxis *axis_y; + // interaction + enum class MouseMode { None, Rubber, Scrub }; + MouseMode mouse_mode = MouseMode::None; + QPoint press_pos; + QRect rubber_rect; + bool resume_after_scrub = false; + QMenu *menu; QAction *split_chart_act; QAction *close_act; - QGraphicsPixmapItem *move_icon; - QGraphicsProxyWidget *close_btn_proxy; - QGraphicsProxyWidget *manage_btn_proxy; + ToolButton *manage_btn; + ToolButton *close_btn; TipLabel *tip_label; std::vector sigs; double cur_sec = 0; SeriesType series_type = SeriesType::Line; - bool is_scrubbing = false; - bool resume_after_scrub = false; QPixmap chart_pixmap; bool can_drop = false; double tooltip_x = -1; diff --git a/tools/cabana/chart/chartswidget.cc b/openpilot/tools/cabana/chart/chartswidget.cc similarity index 79% rename from tools/cabana/chart/chartswidget.cc rename to openpilot/tools/cabana/chart/chartswidget.cc index 44dca42152..3144c5132d 100644 --- a/tools/cabana/chart/chartswidget.cc +++ b/openpilot/tools/cabana/chart/chartswidget.cc @@ -1,11 +1,12 @@ #include "tools/cabana/chart/chartswidget.h" +#include "tools/cabana/dbc/dbcqt.h" #include #include #include #include -#include +#include #include #include @@ -71,11 +72,14 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { range_slider_action = toolbar->addWidget(range_slider); // zoom controls - zoom_undo_stack = new QUndoStack(this); - toolbar->addAction(undo_zoom_action = zoom_undo_stack->createUndoAction(this)); - undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise")); - toolbar->addAction(redo_zoom_action = zoom_undo_stack->createRedoAction(this)); - redo_zoom_action->setIcon(utils::icon("arrow-clockwise")); + undo_zoom_action = toolbar->addAction(utils::icon("arrow-counterclockwise"), tr("Undo Zoom"), [this]() { zoom_undo_stack.undo(); }); + redo_zoom_action = toolbar->addAction(utils::icon("arrow-clockwise"), tr("Redo Zoom"), [this]() { zoom_undo_stack.redo(); }); + undo_zoom_action->setEnabled(false); + redo_zoom_action->setEnabled(false); + zoom_undo_stack.setCallbacks({.index_changed = [this]() { + undo_zoom_action->setEnabled(zoom_undo_stack.canUndo()); + redo_zoom_action->setEnabled(zoom_undo_stack.canRedo()); + }}); reset_zoom_action = toolbar->addWidget(reset_zoom_btn = new ToolButton("zoom-out", tr("Reset Zoom"))); reset_zoom_btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); @@ -88,8 +92,6 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { tabbar->setAutoHide(true); tabbar->setExpanding(false); tabbar->setDrawBase(true); - tabbar->setAcceptDrops(true); - tabbar->setChangeCurrentOnDrag(true); tabbar->setUsesScrollButtons(true); main_layout->addWidget(tabbar); @@ -104,6 +106,11 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { charts_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); main_layout->addWidget(charts_scroll); + // chart drag preview + drag_preview = new QLabel(this); + drag_preview->setAttribute(Qt::WA_TransparentForMouseEvents); + drag_preview->hide(); + // init settings current_theme = settings.theme; column_count = std::clamp(settings.chart_column_count, 1, MAX_COLUMN_COUNT); @@ -115,7 +122,7 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { align_timer->setSingleShot(true); QObject::connect(align_timer, &QTimer::timeout, this, &ChartsWidget::alignCharts); QObject::connect(auto_scroll_timer, &QTimer::timeout, this, &ChartsWidget::doAutoScroll); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &ChartsWidget::removeAll); QObject::connect(can, &AbstractStream::eventsMerged, this, &ChartsWidget::eventsMerged); QObject::connect(can, &AbstractStream::msgsReceived, this, &ChartsWidget::updateState); QObject::connect(can, &AbstractStream::seeking, this, &ChartsWidget::updateState); @@ -185,7 +192,7 @@ void ChartsWidget::timeRangeChanged(const std::optionalsetTimeRange(std::nullopt); - zoom_undo_stack->clear(); + zoom_undo_stack.clear(); } QRect ChartsWidget::chartVisibleRect(ChartView *chart) { @@ -255,10 +262,6 @@ void ChartsWidget::settingChanged() { if (std::exchange(current_theme, settings.theme) != current_theme) { undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise")); redo_zoom_action->setIcon(utils::icon("arrow-clockwise")); - auto theme = utils::isDarkTheme() ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight; - for (auto c : charts) { - c->setTheme(theme); - } } if (range_slider->maximum() != settings.max_cached_minutes * 60) { range_slider->setRange(1, settings.max_cached_minutes * 60); @@ -306,12 +309,8 @@ void ChartsWidget::splitChart(ChartView *src_chart) { int pos = std::find(charts.begin(), charts.end(), src_chart) - charts.begin() + 1; for (auto it = src_chart->sigs.begin() + 1; it != src_chart->sigs.end(); /**/) { auto c = createChart(pos); - src_chart->chart()->removeSeries(it->series); - // Restore to the original color - it->series->setColor(it->sig->color); - - c->addSeries(it->series); + it->color = toQColor(it->sig->color); c->sigs.emplace_back(std::move(*it)); c->updateAxisY(); c->updateTitle(); @@ -319,6 +318,7 @@ void ChartsWidget::splitChart(ChartView *src_chart) { } src_chart->updateAxisY(); src_chart->updateTitle(); + updateState(); QTimer::singleShot(0, src_chart, &ChartView::resetChartCache); } } @@ -389,7 +389,87 @@ void ChartsWidget::updateLayout(bool force) { } } -void ChartsWidget::startAutoScroll() { +void ChartsWidget::startChartDrag(ChartView *chart, const QPoint &global_pos) { + stopAutoScroll(); + drag = {.source = chart, .press_pos = global_pos}; + QPixmap px = chart->grab().scaledToWidth(CHART_MIN_WIDTH * chart->devicePixelRatio(), Qt::SmoothTransformation); + drag_preview->setPixmap(px); + drag_preview->resize(px.size() / px.devicePixelRatio()); +} + +void ChartsWidget::dragChartMove(const QPoint &global_pos) { + if (!drag.active) { + if ((global_pos - drag.press_pos).manhattanLength() < QApplication::startDragDistance()) return; + drag.active = true; + drag_preview->show(); + drag_preview->raise(); + } + drag_preview->move(mapFromGlobal(global_pos) + QPoint(5, 5)); + + // hovering a tab switches to it so the chart can be dropped into another tab + int tab = tabbar->tabAt(tabbar->mapFromGlobal(global_pos)); + if (tab >= 0 && tab != tabbar->currentIndex()) { + tabbar->setCurrentIndex(tab); + } + + const QPoint container_pos = charts_container->mapFromGlobal(global_pos); + ChartView *target = nullptr; + for (auto c : currentCharts()) { + if (c != drag.source && c->isVisible() && c->geometry().contains(container_pos)) { + target = c; + break; + } + } + if (std::exchange(drop_target, target) != target) { + for (auto c : charts) c->setDropHighlight(c == target); + } + bool in_viewport = charts_scroll->viewport()->rect().contains(charts_scroll->viewport()->mapFromGlobal(global_pos)); + bool on_background = !target && in_viewport && !charts_container->childAt(container_pos); + charts_container->drawDropIndicator(on_background ? container_pos : QPoint()); + + if (in_viewport) { + startAutoScroll(global_pos); + } +} + +void ChartsWidget::cancelChartDrag() { + drag = {}; + stopAutoScroll(); + drag_preview->hide(); + charts_container->drawDropIndicator({}); + if (auto target = std::exchange(drop_target, nullptr)) target->setDropHighlight(false); +} + +void ChartsWidget::dragChartRelease(const QPoint &global_pos) { + ChartView *source = drag.source; + bool active = drag.active; + ChartView *target = drop_target; + cancelChartDrag(); + if (!active) return; + + const QPoint container_pos = charts_container->mapFromGlobal(global_pos); + bool in_viewport = charts_scroll->viewport()->rect().contains(charts_scroll->viewport()->mapFromGlobal(global_pos)); + if (target) { + // merge source into target + target->takeSignalsFrom(source); + } else if (in_viewport && !charts_container->childAt(container_pos)) { + // reorder within the current tab + auto w = charts_container->getDropAfter(container_pos); + if (w != source) { + for (auto &[_, list] : tab_charts) { + list.erase(std::remove(list.begin(), list.end(), source), list.end()); + } + auto &cur = currentCharts(); + int to = w ? std::find(cur.begin(), cur.end(), w) - cur.begin() + 1 : 0; + cur.insert(cur.begin() + to, source); + updateLayout(true); + updateTabBar(); + } + } +} + +void ChartsWidget::startAutoScroll(const QPoint &global_pos) { + auto_scroll_pos = global_pos; auto_scroll_timer->start(50); } @@ -405,7 +485,7 @@ void ChartsWidget::doAutoScroll() { } int value = scroll->value(); - QPoint pos = charts_scroll->viewport()->mapFromGlobal(QCursor::pos()); + QPoint pos = charts_scroll->viewport()->mapFromGlobal(auto_scroll_pos); QRect area = charts_scroll->viewport()->rect(); if (pos.y() - area.top() < settings.chart_height / 2) { @@ -413,16 +493,11 @@ void ChartsWidget::doAutoScroll() { } else if (area.bottom() - pos.y() < settings.chart_height / 2) { scroll->setValue(value + auto_scroll_count); } - bool vertical_unchanged = value == scroll->value(); - if (vertical_unchanged) { + if (value == scroll->value()) { stopAutoScroll(); - } else { - // mouseMoveEvent to updates the drag-selection rectangle - const QPoint globalPos = charts_scroll->viewport()->mapToGlobal(pos); - const QPoint windowPos = charts_scroll->window()->mapFromGlobal(globalPos); - QMouseEvent mm(QEvent::MouseMove, pos, windowPos, globalPos, - Qt::NoButton, Qt::LeftButton, Qt::NoModifier, Qt::MouseEventSynthesizedByQt); - QApplication::sendEvent(charts_scroll->viewport(), &mm); + } else if (chartDragActive()) { + // refresh the drop indicator/target at the new scroll position + dragChartMove(auto_scroll_pos); } } @@ -439,11 +514,14 @@ void ChartsWidget::newChart() { for (auto it : items) { c->addSignal(it->msg_id, it->sig); } + updateState(); } } } void ChartsWidget::removeChart(ChartView *chart) { + if (drag.source == chart) cancelChartDrag(); + if (drop_target == chart) drop_target = nullptr; charts.erase(std::remove(charts.begin(), charts.end(), chart), charts.end()); chart->deleteLater(); for (auto &[_, list] : tab_charts) { @@ -483,6 +561,19 @@ void ChartsWidget::alignCharts() { } bool ChartsWidget::eventFilter(QObject *o, QEvent *e) { + // route all mouse events to the chart drag, even when the source chart is hidden by a tab switch + if (chartDragActive()) { + if (e->type() == QEvent::MouseMove) { + dragChartMove(static_cast(e)->globalPos()); + return true; + } else if (e->type() == QEvent::MouseButtonRelease && static_cast(e)->button() == Qt::LeftButton) { + dragChartRelease(static_cast(e)->globalPos()); + return false; // let the release through so Qt clears the implicit mouse grab + } else if (e->type() == QEvent::MouseButtonPress || e->type() == QEvent::MouseButtonRelease) { + return true; // swallow other buttons during the drag + } + } + if (!value_tip_visible_) return false; if (e->type() == QEvent::MouseMove) { @@ -491,7 +582,7 @@ bool ChartsWidget::eventFilter(QObject *o, QEvent *e) { for (const auto &c : charts) { auto local_pos = c->mapFromGlobal(global_pos); - if (c->chart()->plotArea().contains(local_pos)) { + if (c->plot_area.contains(local_pos)) { if (on_tip) { showValueTip(c->secondsAtPoint(local_pos)); } @@ -523,13 +614,14 @@ bool ChartsWidget::event(QEvent *event) { break; case QEvent::WindowDeactivate: case QEvent::FocusOut: + if (chartDragActive()) cancelChartDrag(); showValueTip(-1); default: break; } if (back_button) { - zoom_undo_stack->undo(); + zoom_undo_stack.undo(); return true; // Return true since the event has been handled } return QFrame::event(event); @@ -538,7 +630,6 @@ bool ChartsWidget::event(QEvent *event) { // ChartsContainer ChartsContainer::ChartsContainer(ChartsWidget *parent) : charts_widget(parent), QWidget(parent) { - setAcceptDrops(true); setBackgroundRole(QPalette::Window); QVBoxLayout *charts_main_layout = new QVBoxLayout(this); charts_main_layout->setContentsMargins(0, CHART_SPACING, 0, CHART_SPACING); @@ -548,33 +639,6 @@ ChartsContainer::ChartsContainer(ChartsWidget *parent) : charts_widget(parent), charts_main_layout->addStretch(0); } -void ChartsContainer::dragEnterEvent(QDragEnterEvent *event) { - if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { - event->acceptProposedAction(); - drawDropIndicator(event->pos()); - } -} - -void ChartsContainer::dropEvent(QDropEvent *event) { - if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { - auto w = getDropAfter(event->pos()); - auto chart = qobject_cast(event->source()); - if (w != chart) { - for (auto &[_, list] : charts_widget->tab_charts) { - list.erase(std::remove(list.begin(), list.end(), chart), list.end()); - } - auto &cur = charts_widget->currentCharts(); - int to = w ? std::find(cur.begin(), cur.end(), w) - cur.begin() + 1 : 0; - cur.insert(cur.begin() + to, chart); - charts_widget->updateLayout(true); - charts_widget->updateTabBar(); - event->acceptProposedAction(); - chart->startAnimation(); - } - drawDropIndicator({}); - } -} - void ChartsContainer::paintEvent(QPaintEvent *ev) { if (!drop_indictor_pos.isNull() && !childAt(drop_indictor_pos)) { QRect r = geometry(); diff --git a/tools/cabana/chart/chartswidget.h b/openpilot/tools/cabana/chart/chartswidget.h similarity index 83% rename from tools/cabana/chart/chartswidget.h rename to openpilot/tools/cabana/chart/chartswidget.h index ef3fbc471a..8b3003dcd6 100644 --- a/tools/cabana/chart/chartswidget.h +++ b/openpilot/tools/cabana/chart/chartswidget.h @@ -8,15 +8,13 @@ #include #include #include -#include -#include #include "tools/cabana/chart/signalselector.h" +#include "tools/cabana/commands.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" const int CHART_MIN_WIDTH = 300; -const QString CHART_MIME_TYPE = "application/x-cabanachartview"; class ChartView; class ChartsWidget; @@ -24,9 +22,6 @@ class ChartsWidget; class ChartsContainer : public QWidget { public: ChartsContainer(ChartsWidget *parent); - void dragEnterEvent(QDragEnterEvent *event) override; - void dropEvent(QDropEvent *event) override; - void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator({}); } void drawDropIndicator(const QPoint &pt) { drop_indictor_pos = pt; update(); } void paintEvent(QPaintEvent *ev) override; ChartView *getDropAfter(const QPoint &pos) const; @@ -69,7 +64,12 @@ private: void eventsMerged(const MessageEventsMap &new_events); void updateState(); void zoomReset(); - void startAutoScroll(); + void startChartDrag(ChartView *chart, const QPoint &global_pos); + void dragChartMove(const QPoint &global_pos); + void dragChartRelease(const QPoint &global_pos); + void cancelChartDrag(); + bool chartDragActive() const { return drag.source != nullptr; } + void startAutoScroll(const QPoint &global_pos); void stopAutoScroll(); void doAutoScroll(); void updateToolBar(); @@ -97,7 +97,7 @@ private: QAction *redo_zoom_action; QAction *reset_zoom_action; ToolButton *reset_zoom_btn; - QUndoStack *zoom_undo_stack; + UndoStack zoom_undo_stack; ToolButton *remove_all_btn; std::vector charts; @@ -110,7 +110,15 @@ private: QAction *columns_action; int column_count = 1; int current_column_count = 0; + struct ChartDrag { + ChartView *source = nullptr; + QPoint press_pos; // global + bool active = false; + } drag; + QLabel *drag_preview; + ChartView *drop_target = nullptr; int auto_scroll_count = 0; + QPoint auto_scroll_pos; QTimer *auto_scroll_timer; QTimer *align_timer; int current_theme = 0; @@ -119,11 +127,10 @@ private: friend class ChartsContainer; }; -class ZoomCommand : public QUndoCommand { +class ZoomCommand : public UndoCommand { public: - ZoomCommand(std::pair range) : range(range), QUndoCommand() { + ZoomCommand(std::pair range) : range(range) { prev_range = can->timeRange(); - setText(QObject::tr("Zoom to %1-%2").arg(range.first, 0, 'f', 2).arg(range.second, 0, 'f', 2)); } void undo() override { can->setTimeRange(prev_range); } void redo() override { can->setTimeRange(range); } diff --git a/tools/cabana/chart/signalselector.cc b/openpilot/tools/cabana/chart/signalselector.cc similarity index 95% rename from tools/cabana/chart/signalselector.cc rename to openpilot/tools/cabana/chart/signalselector.cc index 6f2fd8de46..85832e796b 100644 --- a/tools/cabana/chart/signalselector.cc +++ b/openpilot/tools/cabana/chart/signalselector.cc @@ -1,6 +1,6 @@ #include "tools/cabana/chart/signalselector.h" +#include "tools/cabana/dbc/dbcqt.h" -#include #include #include #include @@ -20,8 +20,6 @@ SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent) msgs_combo->setEditable(true); msgs_combo->lineEdit()->setPlaceholderText(tr("Select a msg...")); msgs_combo->setInsertPolicy(QComboBox::NoInsert); - msgs_combo->completer()->setCompletionMode(QCompleter::PopupCompletion); - msgs_combo->completer()->setFilterMode(Qt::MatchContains); main_layout->addWidget(available_list = new QListWidget(this), 2, 0); @@ -92,7 +90,7 @@ void SignalSelector::updateAvailableList(int index) { } void SignalSelector::addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name) { - QString text = QString(" %1").arg(sig->color.name(), QString::fromStdString(sig->name)); + QString text = QString(" %1").arg(toQColor(sig->color).name(), QString::fromStdString(sig->name)); if (show_msg_name) text += QString(" %0 %1").arg(QString::fromStdString(msgName(id)), QString::fromStdString(id.toString())); QLabel *label = new QLabel(text); diff --git a/tools/cabana/chart/signalselector.h b/openpilot/tools/cabana/chart/signalselector.h similarity index 100% rename from tools/cabana/chart/signalselector.h rename to openpilot/tools/cabana/chart/signalselector.h diff --git a/tools/cabana/chart/sparkline.cc b/openpilot/tools/cabana/chart/sparkline.cc similarity index 98% rename from tools/cabana/chart/sparkline.cc rename to openpilot/tools/cabana/chart/sparkline.cc index 91435cd5ac..f5bef0fc2e 100644 --- a/tools/cabana/chart/sparkline.cc +++ b/openpilot/tools/cabana/chart/sparkline.cc @@ -31,7 +31,7 @@ void Sparkline::update(const cabana::Signal *sig, CanEventIter first, CanEventIt } freq_ = points_.size() / std::max(points_.back().x() - points_.front().x(), 1.0); - render(sig->color, range, size); + render(toQColor(sig->color), range, size); } void Sparkline::render(const QColor &color, int range, QSize size) { diff --git a/tools/cabana/chart/sparkline.h b/openpilot/tools/cabana/chart/sparkline.h similarity index 100% rename from tools/cabana/chart/sparkline.h rename to openpilot/tools/cabana/chart/sparkline.h diff --git a/tools/cabana/chart/tiplabel.cc b/openpilot/tools/cabana/chart/tiplabel.cc similarity index 100% rename from tools/cabana/chart/tiplabel.cc rename to openpilot/tools/cabana/chart/tiplabel.cc diff --git a/tools/cabana/chart/tiplabel.h b/openpilot/tools/cabana/chart/tiplabel.h similarity index 100% rename from tools/cabana/chart/tiplabel.h rename to openpilot/tools/cabana/chart/tiplabel.h diff --git a/tools/cabana/commands.cc b/openpilot/tools/cabana/commands.cc similarity index 55% rename from tools/cabana/commands.cc rename to openpilot/tools/cabana/commands.cc index f158528b51..b47bf90b0c 100644 --- a/tools/cabana/commands.cc +++ b/openpilot/tools/cabana/commands.cc @@ -1,20 +1,81 @@ -#include - #include "tools/cabana/commands.h" +#include + +// UndoStack + +void UndoStack::push(UndoCommand *cmd) { + commands_.resize(index_); // drop any redoable commands + if (clean_index_ > index_) clean_index_ = -1; + commands_.emplace_back(cmd); + cmd->redo(); + setIndex(index_ + 1); +} + +void UndoStack::undo() { + if (!canUndo()) return; + commands_[index_ - 1]->undo(); + setIndex(index_ - 1); +} + +void UndoStack::redo() { + if (!canRedo()) return; + commands_[index_]->redo(); + setIndex(index_ + 1); +} + +void UndoStack::clear() { + bool was_clean = isClean(); + commands_.clear(); + index_ = clean_index_ = 0; + if (callbacks_.index_changed) callbacks_.index_changed(); + if (!was_clean && callbacks_.clean_changed) callbacks_.clean_changed(true); +} + +void UndoStack::setClean() { + if (!isClean()) { + clean_index_ = index_; + if (callbacks_.clean_changed) callbacks_.clean_changed(true); + } +} + +void UndoStack::setIndex(int index) { + bool was_clean = isClean(); + index_ = index; + if (callbacks_.index_changed) callbacks_.index_changed(); + if (isClean() != was_clean && callbacks_.clean_changed) callbacks_.clean_changed(isClean()); +} + +UndoStack *UndoStack::instance() { + static UndoStack undo_stack; + return &undo_stack; +} + +QtUndoNotifier::QtUndoNotifier(QObject *parent) : QObject(parent) { + UndoStack::instance()->setCallbacks({ + .index_changed = [this]() { emit indexChanged(); }, + .clean_changed = [this](bool clean) { emit cleanChanged(clean); }, + }); +} + +QtUndoNotifier *undoNotifier() { + static QtUndoNotifier notifier; + return ¬ifier; +} + // EditMsgCommand EditMsgCommand::EditMsgCommand(const MessageId &id, const std::string &name, int size, - const std::string &node, const std::string &comment, QUndoCommand *parent) - : id(id), new_name(name), new_size(size), new_node(node), new_comment(comment), QUndoCommand(parent) { + const std::string &node, const std::string &comment) + : id(id), new_name(name), new_size(size), new_node(node), new_comment(comment) { if (auto msg = dbc()->msg(id)) { old_name = msg->name; old_size = msg->size; old_node = msg->transmitter; old_comment = msg->comment; - setText(QObject::tr("edit message %1:%2").arg(QString::fromStdString(name)).arg(id.address)); + text = "edit message " + name + ":" + std::to_string(id.address); } else { - setText(QObject::tr("new message %1:%2").arg(QString::fromStdString(name)).arg(id.address)); + text = "new message " + name + ":" + std::to_string(id.address); } } @@ -31,10 +92,10 @@ void EditMsgCommand::redo() { // RemoveMsgCommand -RemoveMsgCommand::RemoveMsgCommand(const MessageId &id, QUndoCommand *parent) : id(id), QUndoCommand(parent) { +RemoveMsgCommand::RemoveMsgCommand(const MessageId &id) : id(id) { if (auto msg = dbc()->msg(id)) { message = *msg; - setText(QObject::tr("remove message %1:%2").arg(QString::fromStdString(message.name)).arg(id.address)); + text = "remove message " + message.name + ":" + std::to_string(id.address); } } @@ -53,9 +114,9 @@ void RemoveMsgCommand::redo() { // AddSigCommand -AddSigCommand::AddSigCommand(const MessageId &id, const cabana::Signal &sig, QUndoCommand *parent) - : id(id), signal(sig), QUndoCommand(parent) { - setText(QObject::tr("add signal %1 to %2:%3").arg(QString::fromStdString(sig.name)).arg(QString::fromStdString(msgName(id))).arg(id.address)); +AddSigCommand::AddSigCommand(const MessageId &id, const cabana::Signal &sig) + : id(id), signal(sig) { + text = "add signal " + sig.name + " to " + msgName(id) + ":" + std::to_string(id.address); } void AddSigCommand::undo() { @@ -75,8 +136,7 @@ void AddSigCommand::redo() { // RemoveSigCommand -RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *sig, QUndoCommand *parent) - : id(id), QUndoCommand(parent) { +RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *sig) : id(id) { sigs.push_back(*sig); if (sig->type == cabana::Signal::Type::Multiplexor) { for (const auto &s : dbc()->msg(id)->sigs) { @@ -85,7 +145,7 @@ RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *si } } } - setText(QObject::tr("remove signal %1 from %2:%3").arg(QString::fromStdString(sig->name)).arg(QString::fromStdString(msgName(id))).arg(id.address)); + text = "remove signal " + sig->name + " from " + msgName(id) + ":" + std::to_string(id.address); } void RemoveSigCommand::undo() { for (const auto &s : sigs) dbc()->addSignal(id, s); } @@ -93,8 +153,8 @@ void RemoveSigCommand::redo() { for (const auto &s : sigs) dbc()->removeSignal(i // EditSignalCommand -EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig, QUndoCommand *parent) - : id(id), QUndoCommand(parent) { +EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig) + : id(id) { sigs.push_back({*sig, new_sig}); if (sig->type == cabana::Signal::Type::Multiplexor && new_sig.type == cabana::Signal::Type::Normal) { // convert all multiplexed signals to normal signals @@ -108,17 +168,8 @@ EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal * } } } - setText(QObject::tr("edit signal %1 in %2:%3").arg(QString::fromStdString(sig->name)).arg(QString::fromStdString(msgName(id))).arg(id.address)); + text = "edit signal " + sig->name + " in " + msgName(id) + ":" + std::to_string(id.address); } void EditSignalCommand::undo() { for (const auto &s : sigs) dbc()->updateSignal(id, s.second.name, s.first); } void EditSignalCommand::redo() { for (const auto &s : sigs) dbc()->updateSignal(id, s.first.name, s.second); } - -namespace UndoStack { - -QUndoStack *instance() { - static QUndoStack *undo_stack = new QUndoStack(qApp); - return undo_stack; -} - -} // namespace UndoStack diff --git a/openpilot/tools/cabana/commands.h b/openpilot/tools/cabana/commands.h new file mode 100644 index 0000000000..200a4f2f5b --- /dev/null +++ b/openpilot/tools/cabana/commands.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "tools/cabana/dbc/dbcmanager.h" +#include "tools/cabana/streams/abstractstream.h" + +class UndoCommand { +public: + virtual ~UndoCommand() = default; + virtual void undo() = 0; + virtual void redo() = 0; + std::string text; +}; + +class UndoStack { +public: + struct Callbacks { + std::function index_changed; + std::function clean_changed; + }; + + void push(UndoCommand *cmd); // takes ownership and calls redo() + void undo(); + void redo(); + void clear(); + void setClean(); + bool isClean() const { return clean_index_ == index_; } + bool canUndo() const { return index_ > 0; } + bool canRedo() const { return index_ < (int)commands_.size(); } + std::string undoText() const { return canUndo() ? commands_[index_ - 1]->text : ""; } + std::string redoText() const { return canRedo() ? commands_[index_]->text : ""; } + void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); } + static UndoStack *instance(); + +private: + void setIndex(int index); + std::vector> commands_; + int index_ = 0; + int clean_index_ = 0; + Callbacks callbacks_; +}; + +// emits Qt signals for the global undo stack +class QtUndoNotifier : public QObject { + Q_OBJECT + +public: + explicit QtUndoNotifier(QObject *parent = nullptr); + +signals: + void indexChanged(); + void cleanChanged(bool clean); +}; + +QtUndoNotifier *undoNotifier(); + +class EditMsgCommand : public UndoCommand { +public: + EditMsgCommand(const MessageId &id, const std::string &name, int size, const std::string &node, + const std::string &comment); + void undo() override; + void redo() override; + +private: + const MessageId id; + std::string old_name, new_name, old_comment, new_comment, old_node, new_node; + int old_size = 0, new_size = 0; +}; + +class RemoveMsgCommand : public UndoCommand { +public: + RemoveMsgCommand(const MessageId &id); + void undo() override; + void redo() override; + +private: + const MessageId id; + cabana::Msg message; +}; + +class AddSigCommand : public UndoCommand { +public: + AddSigCommand(const MessageId &id, const cabana::Signal &sig); + void undo() override; + void redo() override; + +private: + const MessageId id; + bool msg_created = false; + cabana::Signal signal = {}; +}; + +class RemoveSigCommand : public UndoCommand { +public: + RemoveSigCommand(const MessageId &id, const cabana::Signal *sig); + void undo() override; + void redo() override; + +private: + const MessageId id; + std::vector sigs; +}; + +class EditSignalCommand : public UndoCommand { +public: + EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig); + void undo() override; + void redo() override; + +private: + const MessageId id; + std::vector> sigs; // {old_sig, new_sig} +}; diff --git a/openpilot/tools/cabana/core/can_data.h b/openpilot/tools/cabana/core/can_data.h new file mode 100644 index 0000000000..38141b1890 --- /dev/null +++ b/openpilot/tools/cabana/core/can_data.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include + +#include "tools/cabana/core/color.h" +#include "tools/cabana/core/message_id.h" + +struct CanData { + void compute(const MessageId &msg_id, const uint8_t *data, int size, double current_sec, + double playback_speed, const std::vector &mask, double frequency = 0); + + double ts = 0.; + uint32_t count = 0; + double freq = 0; + std::vector dat; + std::vector colors; + + struct ByteLastChange { + double ts = 0; + int delta = 0; + int same_delta_counter = 0; + bool suppressed = false; + }; + std::vector last_changes; + std::vector> bit_flip_counts; + double last_freq_update_ts = 0; +}; + +struct CanEvent { + uint8_t src; + uint32_t address; + uint64_t mono_time; + uint8_t size; + uint8_t dat[]; +}; + +struct CompareCanEvent { + constexpr bool operator()(const CanEvent *const event, uint64_t ts) const { return event->mono_time < ts; } + constexpr bool operator()(uint64_t ts, const CanEvent *const event) const { return ts < event->mono_time; } +}; + +using MessageEventsMap = std::unordered_map>; +using CanEventIter = std::vector::const_iterator; diff --git a/openpilot/tools/cabana/core/color.h b/openpilot/tools/cabana/core/color.h new file mode 100644 index 0000000000..c29704dd44 --- /dev/null +++ b/openpilot/tools/cabana/core/color.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include + +struct CabanaColor { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + uint8_t a = 255; + + constexpr CabanaColor() = default; + constexpr CabanaColor(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha = 255) + : r(red), g(green), b(blue), a(alpha) {} + + static CabanaColor fromHsv(float hue, float saturation, float value, float alpha = 1.0f) { + const float h = hue - std::floor(hue); + const float c = value * saturation; + const float x = c * (1.0f - std::fabs(std::fmod(h * 6.0f, 2.0f) - 1.0f)); + const float m = value - c; + float red = 0, green = 0, blue = 0; + switch (static_cast(h * 6.0f) % 6) { + case 0: red = c; green = x; break; + case 1: red = x; green = c; break; + case 2: green = c; blue = x; break; + case 3: green = x; blue = c; break; + case 4: red = x; blue = c; break; + default: red = c; blue = x; break; + } + auto channel = [m](float v) { return static_cast(std::clamp((v + m) * 255.0f, 0.0f, 255.0f) + 0.5f); }; + return {channel(red), channel(green), channel(blue), + static_cast(std::clamp(alpha * 255.0f, 0.0f, 255.0f) + 0.5f)}; + } + + CabanaColor darker(int factor = 200) const { + if (factor <= 0) return *this; + if (factor < 100) return lighter(10000 / factor); + auto [hue, saturation, value] = hsv(); + return fromHsv(hue, saturation, value * 100.0f / factor, a / 255.0f); + } + + CabanaColor lighter(int factor = 150) const { + if (factor <= 0) return *this; + if (factor < 100) return darker(10000 / factor); + auto [hue, saturation, value] = hsv(); + const float scaled_value = value * factor / 100.0f; + if (scaled_value > 1.0f) saturation = std::max(0.0f, saturation - (scaled_value - 1.0f)); + return fromHsv(hue, saturation, std::min(1.0f, scaled_value), a / 255.0f); + } + + constexpr int red() const { return r; } + constexpr int green() const { return g; } + constexpr int blue() const { return b; } + constexpr int alpha() const { return a; } + float alphaF() const { return a / 255.0f; } + void setAlphaF(float alpha) { a = static_cast(std::clamp(alpha * 255.0f, 0.0f, 255.0f) + 0.5f); } + + constexpr bool operator==(const CabanaColor &other) const { + return r == other.r && g == other.g && b == other.b && a == other.a; + } + +private: + struct Hsv { float hue; float saturation; float value; }; + Hsv hsv() const { + const float red = r / 255.0f, green = g / 255.0f, blue = b / 255.0f; + const float maximum = std::max({red, green, blue}); + const float minimum = std::min({red, green, blue}); + const float delta = maximum - minimum; + float hue = 0; + if (delta > 0) { + if (maximum == red) hue = std::fmod((green - blue) / delta, 6.0f) / 6.0f; + else if (maximum == green) hue = ((blue - red) / delta + 2.0f) / 6.0f; + else hue = ((red - green) / delta + 4.0f) / 6.0f; + if (hue < 0) hue += 1.0f; + } + return {hue, maximum == 0 ? 0 : delta / maximum, maximum}; + } +}; diff --git a/openpilot/tools/cabana/core/message_id.h b/openpilot/tools/cabana/core/message_id.h new file mode 100644 index 0000000000..ecac279631 --- /dev/null +++ b/openpilot/tools/cabana/core/message_id.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include +#include +#include +#include + +constexpr int INVALID_SOURCE = 0xff; + +struct MessageId { + uint8_t source = 0; + uint32_t address = 0; + std::string toString() const { char b[64]; snprintf(b, sizeof(b), "%u:%X", source, address); return b; } + static MessageId fromString(const std::string &s) { + const auto p = s.find(':'); + if (p == std::string::npos) return {}; + return {.source = static_cast(std::stoul(s.substr(0, p))), .address = static_cast(std::stoul(s.substr(p + 1), nullptr, 16))}; + } + bool operator==(const MessageId &o) const { return source == o.source && address == o.address; } + bool operator!=(const MessageId &o) const { return !(*this == o); } + bool operator<(const MessageId &o) const { return std::tie(source, address) < std::tie(o.source, o.address); } + bool operator>(const MessageId &o) const { return o < *this; } +}; + +template <> struct std::hash { + size_t operator()(const MessageId &id) const noexcept { return std::hash{}(id.source) ^ (std::hash{}(id.address) << 1); } +}; diff --git a/openpilot/tools/cabana/core/settings.h b/openpilot/tools/cabana/core/settings.h new file mode 100644 index 0000000000..cad152de92 --- /dev/null +++ b/openpilot/tools/cabana/core/settings.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +constexpr int LIGHT_THEME = 1; +constexpr int DARK_THEME = 2; + +struct CabanaSettingsState { + enum DragDirection { MsbFirst, LsbFirst, AlwaysLE, AlwaysBE }; + + bool absolute_time = false; + int fps = 10; + int max_cached_minutes = 30; + int chart_height = 200; + int chart_column_count = 1; + int chart_range = 3 * 60; + int chart_series_type = 0; + int theme = 0; + int sparkline_range = 15; + bool multiple_lines_hex = false; + bool log_livestream = true; + bool suppress_defined_signals = false; + std::string log_path; + std::string last_dir; + std::string last_route_dir; + std::vector recent_files; + DragDirection drag_direction = MsbFirst; + + std::string recent_dbc_file; + std::string active_msg_id; + std::vector selected_msg_ids; + std::vector active_charts; +}; diff --git a/tools/cabana/dbc/dbc.cc b/openpilot/tools/cabana/dbc/dbc.cc similarity index 94% rename from tools/cabana/dbc/dbc.cc rename to openpilot/tools/cabana/dbc/dbc.cc index 8e41cf54e3..c8a794c73f 100644 --- a/tools/cabana/dbc/dbc.cc +++ b/openpilot/tools/cabana/dbc/dbc.cc @@ -1,8 +1,18 @@ #include "tools/cabana/dbc/dbc.h" #include +#include -#include "tools/cabana/utils/util.h" +namespace { +int numDecimals(double value) { + int decimals = 0; + while (decimals < 6 && std::fabs(value - std::round(value)) > 1e-9) { + value *= 10.0; + ++decimals; + } + return decimals; +} +} // cabana::Msg @@ -135,8 +145,8 @@ void cabana::Signal::update() { float s = 0.25 + 0.25 * (float)(hash & 0xff) / 255.0; float v = 0.75 + 0.25 * (float)((hash >> 8) & 0xff) / 255.0; - color = QColor::fromHsvF(h, s, v); - precision = std::max(num_decimals(factor), num_decimals(offset)); + color = CabanaColor::fromHsv(h, s, v); + precision = std::max(numDecimals(factor), numDecimals(offset)); } std::string cabana::Signal::formatValue(double value, bool with_unit) const { diff --git a/tools/cabana/dbc/dbc.h b/openpilot/tools/cabana/dbc/dbc.h similarity index 65% rename from tools/cabana/dbc/dbc.h rename to openpilot/tools/cabana/dbc/dbc.h index a10e7871fe..585325d391 100644 --- a/tools/cabana/dbc/dbc.h +++ b/openpilot/tools/cabana/dbc/dbc.h @@ -8,58 +8,14 @@ #include #include -#include -#include +#include "tools/cabana/core/color.h" +#include "tools/cabana/core/message_id.h" const std::string UNTITLED = "untitled"; const std::string DEFAULT_NODE_NAME = "XXX"; constexpr int CAN_MAX_DATA_BYTES = 64; -struct MessageId { - uint8_t source = 0; - uint32_t address = 0; - - std::string toString() const { - char buf[64]; - snprintf(buf, sizeof(buf), "%u:%X", source, address); - return buf; - } - - inline static MessageId fromString(const std::string &str) { - auto pos = str.find(':'); - if (pos == std::string::npos) return {}; - return MessageId{.source = uint8_t(std::stoul(str.substr(0, pos))), - .address = uint32_t(std::stoul(str.substr(pos + 1), nullptr, 16))}; - } - - bool operator==(const MessageId &other) const { - return source == other.source && address == other.address; - } - - bool operator!=(const MessageId &other) const { - return !(*this == other); - } - - bool operator<(const MessageId &other) const { - return std::tie(source, address) < std::tie(other.source, other.address); - } - - bool operator>(const MessageId &other) const { - return std::tie(source, address) > std::tie(other.source, other.address); - } -}; - -Q_DECLARE_METATYPE(MessageId); - -template <> -struct std::hash { - std::size_t operator()(const MessageId &k) const noexcept { - return std::hash{}(k.source) ^ (std::hash{}(k.address) << 1); - } -}; - typedef std::vector> ValueDescription; -Q_DECLARE_METATYPE(ValueDescription); namespace cabana { @@ -92,7 +48,7 @@ public: std::string receiver_name; ValueDescription val_desc; int precision = 0; - QColor color; + CabanaColor color; // Multiplexed int multiplex_value = 0; diff --git a/openpilot/tools/cabana/dbc/dbcfile.cc b/openpilot/tools/cabana/dbc/dbcfile.cc new file mode 100644 index 0000000000..99a5b71822 --- /dev/null +++ b/openpilot/tools/cabana/dbc/dbcfile.cc @@ -0,0 +1,271 @@ +#include "tools/cabana/dbc/dbcfile.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::string trim(const std::string &value) { + const auto first = value.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) return {}; + return value.substr(first, value.find_last_not_of(" \t\r\n") - first + 1); +} + +bool startsWith(const std::string &value, const char *prefix) { + return value.rfind(prefix, 0) == 0; +} + +std::string unescapeComment(std::string value) { + for (size_t pos = 0; (pos = value.find("\\\"", pos)) != std::string::npos; ++pos) { + value.replace(pos, 2, "\""); + } + return trim(value); +} + +bool commentComplete(const std::string &line) { + bool escaped = false; + for (size_t i = 0; i < line.size(); ++i) { + if (line[i] == '\\' && !escaped) { + escaped = true; + continue; + } + if (line[i] == '"' && !escaped) { + size_t next = line.find_first_not_of(" \t\r\n", i + 1); + if (next != std::string::npos && line[next] == ';') return true; + } + escaped = false; + } + return false; +} + +} // namespace + +DBCFile::DBCFile(const std::string &dbc_file_name) { + std::ifstream file(dbc_file_name, std::ios::binary); + if (!file) throw std::runtime_error("Failed to open file."); + filename = dbc_file_name; + name_ = std::filesystem::path(dbc_file_name).stem().string(); + parse(std::string(std::istreambuf_iterator(file), std::istreambuf_iterator())); +} + +DBCFile::DBCFile(const std::string &name, const std::string &content) : name_(name) { + parse(content); +} + +bool DBCFile::save() { + assert(!filename.empty()); + return writeContents(filename); +} + +bool DBCFile::saveAs(const std::string &new_filename) { + filename = new_filename; + return save(); +} + +bool DBCFile::writeContents(const std::string &fn) { + std::ofstream file(fn, std::ios::binary | std::ios::trunc); + if (!file) return false; + file << generateDBC(); + return file.good(); +} + +void DBCFile::updateMsg(const MessageId &id, const std::string &name, uint32_t size, + const std::string &node, const std::string &comment) { + auto &m = msgs[id.address]; + m.address = id.address; + m.name = name; + m.size = size; + m.transmitter = node.empty() ? DEFAULT_NODE_NAME : node; + m.comment = comment; +} + +cabana::Msg *DBCFile::msg(uint32_t address) { + auto it = msgs.find(address); + return it != msgs.end() ? &it->second : nullptr; +} + +cabana::Msg *DBCFile::msg(const std::string &name) { + auto it = std::find_if(msgs.begin(), msgs.end(), [&name](auto &m) { return m.second.name == name; }); + return it != msgs.end() ? &it->second : nullptr; +} + +cabana::Signal *DBCFile::signal(uint32_t address, const std::string &name) { + auto m = msg(address); + return m ? m->sig(name) : nullptr; +} + +void DBCFile::parse(const std::string &content) { + msgs.clear(); + header.clear(); + std::istringstream input(content); + std::string raw_line; + cabana::Msg *current_msg = nullptr; + int multiplexor_cnt = 0; + int line_num = 0; + bool seen_first = false; + + while (std::getline(input, raw_line)) { + ++line_num; + const size_t first_nonspace = raw_line.find_first_not_of(" \t\r"); + std::string line = first_nonspace == std::string::npos ? std::string() : raw_line.substr(first_nonspace); + const int statement_line = line_num; + if ((startsWith(line, "CM_ BO_") || startsWith(line, "CM_ SG_ ")) && !commentComplete(line)) { + std::string continuation; + while (std::getline(input, continuation)) { + ++line_num; + line += "\n" + continuation; + if (commentComplete(line)) break; + } + } + + bool seen = true; + try { + if (startsWith(line, "BO_ ")) { + multiplexor_cnt = 0; + current_msg = parseBO(line); + } else if (startsWith(line, "SG_ ")) { + parseSG(line, current_msg, multiplexor_cnt); + } else if (startsWith(line, "VAL_ ")) { + parseVAL(line); + } else if (startsWith(line, "CM_ BO_")) { + parseCM_BO(line); + } else if (startsWith(line, "CM_ SG_ ")) { + parseCM_SG(line); + } else { + seen = false; + } + } catch (const std::exception &e) { + throw std::runtime_error("[" + filename + ":" + std::to_string(statement_line) + "]" + e.what() + ": " + line); + } + if (seen) seen_first = true; + else if (!seen_first) header += raw_line + "\n"; + } + for (auto &[_, message] : msgs) message.update(); +} + +cabana::Msg *DBCFile::parseBO(const std::string &line) { + static const std::regex pattern(R"(^BO_ ([[:alnum:]_]+) ([[:alnum:]_]+) *: ([[:alnum:]_]+) ([[:alnum:]_]+))"); + std::smatch match; + if (!std::regex_search(line, match, pattern)) throw std::runtime_error("Invalid BO_ line format"); + const uint32_t address = std::stoul(match[1].str()); + if (msgs.count(address)) throw std::runtime_error("Duplicate message address: " + std::to_string(address)); + auto &message = msgs[address]; + message.address = address; + message.name = match[2].str(); + message.size = std::stoul(match[3].str()); + message.transmitter = trim(match[4].str()); + return &message; +} + +void DBCFile::parseSG(const std::string &line, cabana::Msg *current_msg, int &multiplexor_cnt) { + static const std::regex pattern(R"dbc(^SG_ ([[:alnum:]_]+)(?: +([[:alnum:]_]+))? *: ([0-9]+)\|([0-9]+)@([0-9]+)([+-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] "(.*)" (.*))dbc"); + if (!current_msg) throw std::runtime_error("No Message"); + std::smatch match; + if (!std::regex_search(line, match, pattern)) throw std::runtime_error("Invalid SG_ line format"); + if (current_msg->sig(match[1].str())) throw std::runtime_error("Duplicate signal name"); + + cabana::Signal signal{}; + const std::string indicator = match[2].str(); + if (!indicator.empty()) { + if (indicator == "M") { + if (++multiplexor_cnt >= 2) throw std::runtime_error("Multiple multiplexor"); + signal.type = cabana::Signal::Type::Multiplexor; + } else { + signal.type = cabana::Signal::Type::Multiplexed; + signal.multiplex_value = indicator.size() > 1 ? std::stoi(indicator.substr(1)) : 0; + } + } + signal.name = match[1].str(); + signal.start_bit = std::stoi(match[3].str()); + signal.size = std::stoi(match[4].str()); + signal.is_little_endian = match[5].str() == "1"; + signal.is_signed = match[6].str() == "-"; + signal.factor = std::stod(match[7].str()); + signal.offset = std::stod(match[8].str()); + signal.min = std::stod(match[9].str()); + signal.max = std::stod(match[10].str()); + signal.unit = match[11].str(); + signal.receiver_name = trim(match[12].str()); + current_msg->sigs.push_back(new cabana::Signal(signal)); +} + +void DBCFile::parseCM_BO(const std::string &line) { + std::istringstream prefix(line.substr(7)); + uint32_t address = 0; + prefix >> address; + const size_t first_quote = line.find('"'); + const size_t last_quote = line.rfind('"'); + if (!prefix || first_quote == std::string::npos || last_quote <= first_quote) { + throw std::runtime_error("Invalid message comment format"); + } + if (auto message = msg(address)) message->comment = unescapeComment(line.substr(first_quote + 1, last_quote - first_quote - 1)); +} + +void DBCFile::parseCM_SG(const std::string &line) { + std::istringstream prefix(line.substr(7)); + uint32_t address = 0; + std::string name; + prefix >> address >> name; + const size_t first_quote = line.find('"'); + const size_t last_quote = line.rfind('"'); + if (!prefix || name.empty() || first_quote == std::string::npos || last_quote <= first_quote) { + throw std::runtime_error("Invalid CM_ SG_ line format"); + } + if (auto sig = signal(address, name)) sig->comment = unescapeComment(line.substr(first_quote + 1, last_quote - first_quote - 1)); +} + +void DBCFile::parseVAL(const std::string &line) { + static const std::regex header_pattern(R"(^VAL_ ([[:alnum:]_]+) ([[:alnum:]_]+) (.*))"); + static const std::regex entry_pattern(R"dbc(([+-]?[0-9]+(?:\.[0-9]+)?)\s+"([^"]*)")dbc"); + std::smatch match; + if (!std::regex_search(line, match, header_pattern)) throw std::runtime_error("invalid VAL_ line format"); + if (auto sig = signal(std::stoul(match[1].str()), match[2].str())) { + const std::string entries = match[3].str(); + for (std::sregex_iterator it(entries.begin(), entries.end(), entry_pattern), end; it != end; ++it) { + sig->val_desc.emplace_back(std::stod((*it)[1].str()), trim((*it)[2].str())); + } + } +} + +std::string DBCFile::generateDBC() { + std::string dbc_string, comment, val_desc; + for (const auto &[address, m] : msgs) { + const std::string &transmitter = m.transmitter.empty() ? DEFAULT_NODE_NAME : m.transmitter; + dbc_string += "BO_ " + std::to_string(address) + " " + m.name + ": " + std::to_string(m.size) + " " + transmitter + "\n"; + if (!m.comment.empty()) { + std::string escaped = m.comment; + for (size_t pos = 0; (pos = escaped.find('"', pos)) != std::string::npos; pos += 2) escaped.replace(pos, 1, "\\\""); + comment += "CM_ BO_ " + std::to_string(address) + " \"" + escaped + "\";\n"; + } + for (auto sig : m.getSignals()) { + std::string mux; + if (sig->type == cabana::Signal::Type::Multiplexor) mux = "M "; + else if (sig->type == cabana::Signal::Type::Multiplexed) mux = "m" + std::to_string(sig->multiplex_value) + " "; + const std::string &receiver = sig->receiver_name.empty() ? DEFAULT_NODE_NAME : sig->receiver_name; + dbc_string += " SG_ " + sig->name + " " + mux + ": " + std::to_string(sig->start_bit) + "|" + std::to_string(sig->size) + "@" + + (sig->is_little_endian ? "1" : "0") + (sig->is_signed ? "-" : "+") + + " (" + doubleToString(sig->factor) + "," + doubleToString(sig->offset) + ")" + + " [" + doubleToString(sig->min) + "|" + doubleToString(sig->max) + "] \"" + sig->unit + "\" " + receiver + "\n"; + if (!sig->comment.empty()) { + std::string escaped = sig->comment; + for (size_t pos = 0; (pos = escaped.find('"', pos)) != std::string::npos; pos += 2) escaped.replace(pos, 1, "\\\""); + comment += "CM_ SG_ " + std::to_string(address) + " " + sig->name + " \"" + escaped + "\";\n"; + } + if (!sig->val_desc.empty()) { + std::string text; + for (const auto &[value, description] : sig->val_desc) { + if (!text.empty()) text += " "; + text += doubleToString(value) + " \"" + description + "\""; + } + val_desc += "VAL_ " + std::to_string(address) + " " + sig->name + " " + text + ";\n"; + } + } + dbc_string += "\n"; + } + return header + dbc_string + comment + val_desc; +} diff --git a/tools/cabana/dbc/dbcfile.h b/openpilot/tools/cabana/dbc/dbcfile.h similarity index 71% rename from tools/cabana/dbc/dbcfile.h rename to openpilot/tools/cabana/dbc/dbcfile.h index decb566abd..13841a94fa 100644 --- a/tools/cabana/dbc/dbcfile.h +++ b/openpilot/tools/cabana/dbc/dbcfile.h @@ -2,7 +2,6 @@ #include #include -#include #include "tools/cabana/dbc/dbc.h" @@ -32,12 +31,12 @@ public: std::string filename; private: - void parse(const QString &content); - cabana::Msg *parseBO(const QString &line); - void parseSG(const QString &line, cabana::Msg *current_msg, int &multiplexor_cnt); - void parseCM_BO(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream); - void parseCM_SG(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream); - void parseVAL(const QString &line); + void parse(const std::string &content); + cabana::Msg *parseBO(const std::string &line); + void parseSG(const std::string &line, cabana::Msg *current_msg, int &multiplexor_cnt); + void parseCM_BO(const std::string &line); + void parseCM_SG(const std::string &line); + void parseVAL(const std::string &line); std::string header; std::map msgs; diff --git a/tools/cabana/dbc/dbcmanager.cc b/openpilot/tools/cabana/dbc/dbcmanager.cc similarity index 81% rename from tools/cabana/dbc/dbcmanager.cc rename to openpilot/tools/cabana/dbc/dbcmanager.cc index 2236a93da1..7a95a4f809 100644 --- a/tools/cabana/dbc/dbcmanager.cc +++ b/openpilot/tools/cabana/dbc/dbcmanager.cc @@ -1,9 +1,10 @@ #include "tools/cabana/dbc/dbcmanager.h" #include +#include #include -bool DBCManager::open(const SourceSet &sources, const std::string &dbc_file_name, QString *error) { +bool DBCManager::open(const SourceSet &sources, const std::string &dbc_file_name, std::string *error) { try { auto it = std::find_if(dbc_files.begin(), dbc_files.end(), [&](auto &f) { return f.second && f.second->filename == dbc_file_name; }); @@ -16,11 +17,11 @@ bool DBCManager::open(const SourceSet &sources, const std::string &dbc_file_name return false; } - emit DBCFileChanged(); + if (callbacks_.file_changed) callbacks_.file_changed(); return true; } -bool DBCManager::open(const SourceSet &sources, const std::string &name, const std::string &content, QString *error) { +bool DBCManager::open(const SourceSet &sources, const std::string &name, const std::string &content, std::string *error) { try { auto file = std::make_shared(name, content); for (auto s : sources) { @@ -31,7 +32,7 @@ bool DBCManager::open(const SourceSet &sources, const std::string &name, const s return false; } - emit DBCFileChanged(); + if (callbacks_.file_changed) callbacks_.file_changed(); return true; } @@ -39,26 +40,26 @@ void DBCManager::close(const SourceSet &sources) { for (auto s : sources) { dbc_files[s] = nullptr; } - emit DBCFileChanged(); + if (callbacks_.file_changed) callbacks_.file_changed(); } void DBCManager::close(DBCFile *dbc_file) { for (auto &[_, f] : dbc_files) { if (f.get() == dbc_file) f = nullptr; } - emit DBCFileChanged(); + if (callbacks_.file_changed) callbacks_.file_changed(); } void DBCManager::closeAll() { dbc_files.clear(); - emit DBCFileChanged(); + if (callbacks_.file_changed) callbacks_.file_changed(); } void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) { if (auto m = msg(id)) { if (auto s = m->addSignal(sig)) { - emit signalAdded(id, s); - emit maskUpdated(); + if (callbacks_.signal_added) callbacks_.signal_added(id, s); + if (callbacks_.mask_updated) callbacks_.mask_updated(); } } } @@ -66,8 +67,8 @@ void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) { void DBCManager::updateSignal(const MessageId &id, const std::string &sig_name, const cabana::Signal &sig) { if (auto m = msg(id)) { if (auto s = m->updateSignal(sig_name, sig)) { - emit signalUpdated(s); - emit maskUpdated(); + if (callbacks_.signal_updated) callbacks_.signal_updated(s); + if (callbacks_.mask_updated) callbacks_.mask_updated(); } } } @@ -75,9 +76,9 @@ void DBCManager::updateSignal(const MessageId &id, const std::string &sig_name, void DBCManager::removeSignal(const MessageId &id, const std::string &sig_name) { if (auto m = msg(id)) { if (auto s = m->sig(sig_name)) { - emit signalRemoved(s); + if (callbacks_.signal_removed) callbacks_.signal_removed(s); m->removeSignal(sig_name); - emit maskUpdated(); + if (callbacks_.mask_updated) callbacks_.mask_updated(); } } } @@ -86,15 +87,15 @@ void DBCManager::updateMsg(const MessageId &id, const std::string &name, uint32_ auto dbc_file = findDBCFile(id); assert(dbc_file); // This should be impossible dbc_file->updateMsg(id, name, size, node, comment); - emit msgUpdated(id); + if (callbacks_.msg_updated) callbacks_.msg_updated(id); } void DBCManager::removeMsg(const MessageId &id) { auto dbc_file = findDBCFile(id); assert(dbc_file); // This should be impossible dbc_file->removeMsg(id); - emit msgRemoved(id); - emit maskUpdated(); + if (callbacks_.msg_removed) callbacks_.msg_removed(id); + if (callbacks_.mask_updated) callbacks_.mask_updated(); } std::string DBCManager::newMsgName(const MessageId &id) { @@ -176,6 +177,6 @@ std::string toString(const SourceSet &ss) { } DBCManager *dbc() { - static DBCManager dbc_manager(nullptr); + static DBCManager dbc_manager; return &dbc_manager; } diff --git a/tools/cabana/dbc/dbcmanager.h b/openpilot/tools/cabana/dbc/dbcmanager.h similarity index 73% rename from tools/cabana/dbc/dbcmanager.h rename to openpilot/tools/cabana/dbc/dbcmanager.h index 4a122073ea..5a09fae03d 100644 --- a/tools/cabana/dbc/dbcmanager.h +++ b/openpilot/tools/cabana/dbc/dbcmanager.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include #include @@ -11,17 +11,23 @@ typedef std::set SourceSet; const SourceSet SOURCE_ALL = {-1}; -const int INVALID_SOURCE = 0xff; inline bool operator<(const std::shared_ptr &l, const std::shared_ptr &r) { return l.get() < r.get(); } -class DBCManager : public QObject { - Q_OBJECT - +class DBCManager { public: - DBCManager(QObject *parent) : QObject(parent) {} - ~DBCManager() {} - bool open(const SourceSet &sources, const std::string &dbc_file_name, QString *error = nullptr); - bool open(const SourceSet &sources, const std::string &name, const std::string &content, QString *error = nullptr); + struct Callbacks { + std::function signal_added; + std::function signal_removed; + std::function signal_updated; + std::function msg_updated; + std::function msg_removed; + std::function file_changed; + std::function mask_updated; + }; + + DBCManager() = default; + bool open(const SourceSet &sources, const std::string &dbc_file_name, std::string *error = nullptr); + bool open(const SourceSet &sources, const std::string &name, const std::string &content, std::string *error = nullptr); void close(const SourceSet &sources); void close(DBCFile *dbc_file); void closeAll(); @@ -48,18 +54,11 @@ public: DBCFile *findDBCFile(const uint8_t source); inline DBCFile *findDBCFile(const MessageId &id) { return findDBCFile(id.source); } std::set allDBCFiles(); - -signals: - void signalAdded(MessageId id, const cabana::Signal *sig); - void signalRemoved(const cabana::Signal *sig); - void signalUpdated(const cabana::Signal *sig); - void msgUpdated(MessageId id); - void msgRemoved(MessageId id); - void DBCFileChanged(); - void maskUpdated(); + void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); } private: std::map> dbc_files; + Callbacks callbacks_; }; DBCManager *dbc(); diff --git a/openpilot/tools/cabana/dbc/dbcqt.cc b/openpilot/tools/cabana/dbc/dbcqt.cc new file mode 100644 index 0000000000..4354caf3f5 --- /dev/null +++ b/openpilot/tools/cabana/dbc/dbcqt.cc @@ -0,0 +1,18 @@ +#include "tools/cabana/dbc/dbcqt.h" + +QtDBCNotifier::QtDBCNotifier(QObject *parent) : QObject(parent) { + dbc()->setCallbacks({ + .signal_added = [this](MessageId id, const cabana::Signal *sig) { emit signalAdded(id, sig); }, + .signal_removed = [this](const cabana::Signal *sig) { emit signalRemoved(sig); }, + .signal_updated = [this](const cabana::Signal *sig) { emit signalUpdated(sig); }, + .msg_updated = [this](MessageId id) { emit msgUpdated(id); }, + .msg_removed = [this](MessageId id) { emit msgRemoved(id); }, + .file_changed = [this]() { emit DBCFileChanged(); }, + .mask_updated = [this]() { emit maskUpdated(); }, + }); +} + +QtDBCNotifier *dbcNotifier() { + static QtDBCNotifier notifier; + return ¬ifier; +} diff --git a/openpilot/tools/cabana/dbc/dbcqt.h b/openpilot/tools/cabana/dbc/dbcqt.h new file mode 100644 index 0000000000..b889f854ed --- /dev/null +++ b/openpilot/tools/cabana/dbc/dbcqt.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include "tools/cabana/dbc/dbcmanager.h" + +Q_DECLARE_METATYPE(MessageId) +Q_DECLARE_METATYPE(ValueDescription) + +class QtDBCNotifier : public QObject { + Q_OBJECT + +public: + explicit QtDBCNotifier(QObject *parent = nullptr); + +signals: + void signalAdded(MessageId id, const cabana::Signal *sig); + void signalRemoved(const cabana::Signal *sig); + void signalUpdated(const cabana::Signal *sig); + void msgUpdated(MessageId id); + void msgRemoved(MessageId id); + void DBCFileChanged(); + void maskUpdated(); +}; + +QtDBCNotifier *dbcNotifier(); diff --git a/tools/cabana/dbc/generate_dbc_json.py b/openpilot/tools/cabana/dbc/generate_dbc_json.py similarity index 99% rename from tools/cabana/dbc/generate_dbc_json.py rename to openpilot/tools/cabana/dbc/generate_dbc_json.py index d002a50ef8..13b6799ffb 100755 --- a/tools/cabana/dbc/generate_dbc_json.py +++ b/openpilot/tools/cabana/dbc/generate_dbc_json.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import argparse import json - from opendbc.car import Bus from opendbc.car.fingerprints import MIGRATION from opendbc.car.values import PLATFORMS diff --git a/openpilot/tools/cabana/deqt.md b/openpilot/tools/cabana/deqt.md new file mode 100644 index 0000000000..3bef2d7ab0 --- /dev/null +++ b/openpilot/tools/cabana/deqt.md @@ -0,0 +1,53 @@ +we're migrating cabana away from Qt and to eventually entirely use imgui + +we are doing it incrementally, in small pieces that are easy to execute and verify. +we will repeat this until we're all done. + +# Cabana Qt API inventory + +these are all still in cabana. we remove them from this list once they're gone. +each bullet is an atomic unit of work. + +our workflow is: +- pick the easiest of the bulleted items from below +- implement it and make] sure it builds +- spin up reviewer agents to review the code in a clean context and a separate one to click around in xvfb as a gui test +- then implement the fixes from the above reviewer agents + +some rules +- do not add more Qt usage ever + +- `QObject`, `QMetaObject`, `QMetaType` +- `QApplication`, `QCoreApplication`, `QGuiApplication` +- `QString`, `QStringList`, `QStringBuilder`, `QChar`, `QLatin1Char` +- `QVariant` +- `QTimer` +- `QWidget`, `QMainWindow`, `QWindow` +- `QDialog`, `QDialogButtonBox`, `QMessageBox`, `QProgressDialog` +- `QFileDialog` +- `QMenu`, `QMenuBar`, `QAction`, `QActionGroup`, `QWidgetAction` +- `QToolBar`, `QToolButton`, `QPushButton` +- `QCheckBox`, `QRadioButton`, `QButtonGroup`, `QAbstractButton` +- `QComboBox`, `QLineEdit`, `QTextEdit`, `QSpinBox`, `QSlider` +- `QLabel`, `QGroupBox`, `QFrame` +- `QTabBar`, `QTabWidget`, `QSplitter`, `QScrollArea`, `QScrollBar` +- `QDockWidget`, `QStatusBar`, `QProgressBar` +- `QFormLayout`, `QGridLayout`, `QHBoxLayout`, `QVBoxLayout` +- `QSizePolicy` +- `QAbstractItemModel`, `QAbstractTableModel`, `QModelIndex` +- `QAbstractItemView`, `QTableView`, `QTreeView` +- `QTableWidget`, `QTableWidgetItem`, `QListWidget`, `QListWidgetItem` +- `QItemSelection`, `QItemSelectionModel`, `QItemSelectionRange` +- `QHeaderView`, `QStyledItemDelegate`, `QStyleOptionViewItem` +- `QValidator`, `QIntValidator` +- `QColor`, `QRgb`, `QPalette` +- `QBrush`, `QPen` +- `QPainter`, `QPainterPath`, `QStylePainter` +- `QImage`, `QPixmap`, `QPixmapCache`, `QStaticText` +- `QFont`, `QFontDatabase`, `QFontMetrics`, `QTextDocument` +- `QStyle`, `QStyleOption`, `QStyleOptionFrame`, `QStyleOptionSlider` +- `QPoint`, `QPointF`, `QRect`, `QRectF`, `QRegion` +- `QSize`, `QSizeF` +- `QEvent`, `QPaintEvent`, `QResizeEvent`, `QShowEvent`, `QCloseEvent` +- `QMouseEvent`, `QWheelEvent`, `QNativeGestureEvent`, `QContextMenuEvent` +- `QKeySequence`, `QShortcut`, `QToolTip` diff --git a/tools/cabana/detailwidget.cc b/openpilot/tools/cabana/detailwidget.cc similarity index 96% rename from tools/cabana/detailwidget.cc rename to openpilot/tools/cabana/detailwidget.cc index 148b059e5b..6b62f54959 100644 --- a/tools/cabana/detailwidget.cc +++ b/openpilot/tools/cabana/detailwidget.cc @@ -1,4 +1,5 @@ #include "tools/cabana/detailwidget.h" +#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -56,8 +57,8 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart QObject::connect(signal_view, &SignalView::highlight, binary_view, &BinaryView::highlight); QObject::connect(tab_widget, &QTabWidget::currentChanged, [this]() { updateState(); }); QObject::connect(can, &AbstractStream::msgsReceived, this, &DetailWidget::updateState); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &DetailWidget::refresh); - QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &DetailWidget::refresh); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &DetailWidget::refresh); + QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &DetailWidget::refresh); QObject::connect(tabbar, &QTabBar::customContextMenuRequested, this, &DetailWidget::showTabBarContextMenu); QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) { if (index != -1) { @@ -210,13 +211,13 @@ void DetailWidget::editMsg() { int size = msg ? msg->size : can->lastMessage(msg_id).dat.size(); EditMessageDialog dlg(msg_id, QString::fromStdString(msgName(msg_id)), size, this); if (dlg.exec()) { - UndoStack::push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed().toStdString(), dlg.size_spin->value(), + UndoStack::instance()->push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed().toStdString(), dlg.size_spin->value(), dlg.node->text().trimmed().toStdString(), dlg.comment_edit->toPlainText().trimmed().toStdString())); } } void DetailWidget::removeMsg() { - UndoStack::push(new RemoveMsgCommand(msg_id)); + UndoStack::instance()->push(new RemoveMsgCommand(msg_id)); } // EditMessageDialog diff --git a/tools/cabana/detailwidget.h b/openpilot/tools/cabana/detailwidget.h similarity index 100% rename from tools/cabana/detailwidget.h rename to openpilot/tools/cabana/detailwidget.h diff --git a/tools/cabana/historylog.cc b/openpilot/tools/cabana/historylog.cc similarity index 93% rename from tools/cabana/historylog.cc rename to openpilot/tools/cabana/historylog.cc index fb79ff9cea..3fd569c648 100644 --- a/tools/cabana/historylog.cc +++ b/openpilot/tools/cabana/historylog.cc @@ -1,4 +1,5 @@ #include "tools/cabana/historylog.h" +#include "tools/cabana/dbc/dbcqt.h" #include @@ -54,7 +55,7 @@ QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, i return unit.isEmpty() ? name : QString("%1 (%2)").arg(name, unit); } else if (role == Qt::BackgroundRole && section > 0 && !isHexMode()) { // Alpha-blend the signal color with the background to ensure contrast - QColor sigColor = sigs[section - 1]->color; + QColor sigColor = toQColor(sigs[section - 1]->color); sigColor.setAlpha(128); return QBrush(sigColor); } @@ -207,8 +208,8 @@ LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) { QObject::connect(value_edit, &QLineEdit::textEdited, this, &LogsWidget::filterChanged); QObject::connect(export_btn, &QToolButton::clicked, this, &LogsWidget::exportToCSV); QObject::connect(can, &AbstractStream::seekedTo, model, &HistoryLogModel::reset); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, model, &HistoryLogModel::reset); - QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, model, &HistoryLogModel::reset); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &HistoryLogModel::reset); + QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, model, &HistoryLogModel::reset); QObject::connect(model, &HistoryLogModel::modelReset, this, &LogsWidget::modelReset); QObject::connect(model, &HistoryLogModel::rowsInserted, [this]() { export_btn->setEnabled(true); }); } @@ -238,11 +239,11 @@ void LogsWidget::filterChanged() { } void LogsWidget::exportToCSV() { - QString dir = QString("%1/%2_%3.csv").arg(settings.last_dir).arg(QString::fromStdString(can->routeName())).arg(QString::fromStdString(msgName(model->msg_id))); + QString dir = QString("%1/%2_%3.csv").arg(QString::fromStdString(settings.last_dir)).arg(QString::fromStdString(can->routeName())).arg(QString::fromStdString(msgName(model->msg_id))); QString fn = QFileDialog::getSaveFileName(this, QString("Export %1 to CSV file").arg(QString::fromStdString(msgName(model->msg_id))), dir, tr("csv (*.csv)")); if (!fn.isEmpty()) { - model->isHexMode() ? utils::exportToCSV(fn, model->msg_id) - : utils::exportSignalsToCSV(fn, model->msg_id); + model->isHexMode() ? utils::exportToCSV(fn.toStdString(), model->msg_id) + : utils::exportSignalsToCSV(fn.toStdString(), model->msg_id); } } diff --git a/tools/cabana/historylog.h b/openpilot/tools/cabana/historylog.h similarity index 98% rename from tools/cabana/historylog.h rename to openpilot/tools/cabana/historylog.h index 1ac6e5bbad..1d3200b200 100644 --- a/tools/cabana/historylog.h +++ b/openpilot/tools/cabana/historylog.h @@ -40,7 +40,7 @@ public: uint64_t mono_time = 0; std::vector sig_values; std::vector data; - std::vector colors; + std::vector colors; }; void fetchData(std::deque::iterator insert_pos, uint64_t from_time, uint64_t min_time); diff --git a/tools/cabana/mainwin.cc b/openpilot/tools/cabana/mainwin.cc similarity index 78% rename from tools/cabana/mainwin.cc rename to openpilot/tools/cabana/mainwin.cc index 39fb979c79..953f573a0b 100644 --- a/tools/cabana/mainwin.cc +++ b/openpilot/tools/cabana/mainwin.cc @@ -1,25 +1,24 @@ #include "tools/cabana/mainwin.h" +#include "tools/cabana/dbc/dbcqt.h" #include +#include +#include #include +#include #include +#include -#include -#include -#include #include -#include -#include #include #include #include #include #include #include -#include #include -#include +#include "json11/json11.hpp" #include "tools/cabana/commands.h" #include "tools/cabana/streamselector.h" #include "tools/cabana/tools/findsignal.h" @@ -36,14 +35,11 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW createShortcuts(); // save default window state to allow resetting it - default_state = saveState(); + default_state = utils::toBytes(saveState()); - // restore states - restoreGeometry(settings.geometry); - if (isMaximized()) { - setGeometry(QApplication::desktop()->availableGeometry(this)); - } - restoreState(settings.window_state); + // restore states; restoreGeometry() itself corrects stale off-screen geometry + restoreGeometry(utils::qbytes(settings.geometry)); + restoreState(utils::qbytes(settings.window_state)); // install handlers static auto static_main_win = this; @@ -52,11 +48,9 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW installDownloadProgressHandler([](uint64_t cur, uint64_t total, bool success) { emit static_main_win->updateProgressBar(cur, total, success); }); - qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &context, const QString &msg) { - if (type == QtDebugMsg) return; - emit static_main_win->showMessage(msg, 2000); + installMessageHandler([](ReplyMsgType type, const std::string msg) { + emit static_main_win->showMessage(QString::fromStdString(msg), 2000); }); - installMessageHandler([](ReplyMsgType type, const std::string msg) { qInfo() << msg.c_str(); }); setStyleSheet(QString(R"(QMainWindow::separator { width: %1px; /* when vertical */ @@ -65,8 +59,8 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW QObject::connect(this, &MainWindow::showMessage, statusBar(), &QStatusBar::showMessage); QObject::connect(this, &MainWindow::updateProgressBar, this, &MainWindow::updateDownloadProgress); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &MainWindow::DBCFileChanged); - QObject::connect(UndoStack::instance(), &QUndoStack::cleanChanged, this, &MainWindow::undoStackCleanChanged); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &MainWindow::DBCFileChanged); + QObject::connect(undoNotifier(), &QtUndoNotifier::cleanChanged, this, &MainWindow::undoStackCleanChanged); QObject::connect(&settings, &Settings::changed, this, &MainWindow::updateStatus); QTimer::singleShot(0, this, [=]() { stream ? openStream(stream, dbc_file) : selectAndOpenStream(); }); @@ -74,9 +68,17 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW } void MainWindow::loadFingerprints() { - QFile json_file(QApplication::applicationDirPath() + "/dbc/car_fingerprint_to_dbc.json"); - if (json_file.open(QIODevice::ReadOnly)) { - fingerprint_to_dbc = QJsonDocument::fromJson(json_file.readAll()); + std::ifstream json_file((QApplication::applicationDirPath() + "/dbc/car_fingerprint_to_dbc.json").toStdString()); + if (!json_file) return; + const std::string contents{std::istreambuf_iterator(json_file), std::istreambuf_iterator()}; + std::string err; + auto doc = json11::Json::parse(contents, err); + if (!err.empty() || !doc.is_object()) return; + fingerprint_to_dbc.clear(); + for (const auto &kv : doc.object_items()) { + if (kv.second.is_string()) { + fingerprint_to_dbc.emplace(kv.first, kv.second.string_value()); + } } } @@ -102,8 +104,17 @@ void MainWindow::createActions() { file_menu->addSeparator(); QMenu *load_opendbc_menu = file_menu->addMenu(tr("Load DBC from commaai/opendbc")); // load_opendbc_menu->setStyleSheet("QMenu { menu-scrollable: true; }"); - for (const auto &dbc_name : QDir(OPENDBC_FILE_PATH).entryList({"*.dbc"}, QDir::Files, QDir::Name)) { - load_opendbc_menu->addAction(dbc_name, [this, name = dbc_name]() { loadDBCFromOpendbc(name); }); + std::vector dbc_names; + std::error_code ec; + for (const auto &entry : std::filesystem::directory_iterator(OPENDBC_FILE_PATH, ec)) { + if (entry.is_regular_file() && entry.path().extension() == ".dbc") { + dbc_names.push_back(entry.path().filename().string()); + } + } + std::sort(dbc_names.begin(), dbc_names.end()); + for (const auto &dbc_name : dbc_names) { + QString name = QString::fromStdString(dbc_name); + load_opendbc_menu->addAction(name, [this, name]() { loadDBCFromOpendbc(name); }); } file_menu->addAction(tr("Load DBC From Clipboard"), [=]() { loadFromClipboard(); }); @@ -121,18 +132,12 @@ void MainWindow::createActions() { // Edit Menu QMenu *edit_menu = menuBar()->addMenu(tr("&Edit")); - auto undo_act = UndoStack::instance()->createUndoAction(this, tr("&Undo")); + undo_act = edit_menu->addAction(tr("&Undo"), []() { UndoStack::instance()->undo(); }); undo_act->setShortcuts(QKeySequence::Undo); - edit_menu->addAction(undo_act); - auto redo_act = UndoStack::instance()->createRedoAction(this, tr("&Redo")); + redo_act = edit_menu->addAction(tr("&Redo"), []() { UndoStack::instance()->redo(); }); redo_act->setShortcuts(QKeySequence::Redo); - edit_menu->addAction(redo_act); - edit_menu->addSeparator(); - - QMenu *commands_menu = edit_menu->addMenu(tr("Command &List")); - QWidgetAction *commands_act = new QWidgetAction(this); - commands_act->setDefaultWidget(new QUndoView(UndoStack::instance())); - commands_menu->addAction(commands_act); + QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &MainWindow::updateUndoRedoActions); + updateUndoRedoActions(); // View Menu QMenu *view_menu = menuBar()->addMenu(tr("&View")); @@ -142,7 +147,7 @@ void MainWindow::createActions() { view_menu->addAction(messages_dock->toggleViewAction()); view_menu->addAction(video_dock->toggleViewAction()); view_menu->addSeparator(); - view_menu->addAction(tr("Reset Window Layout"), [this]() { restoreState(default_state); }); + view_menu->addAction(tr("Reset Window Layout"), [this]() { restoreState(utils::qbytes(default_state)); }); // Tools Menu tools_menu = menuBar()->addMenu(tr("&Tools")); @@ -189,7 +194,7 @@ void MainWindow::createDockWidgets() { video_splitter->addWidget(charts_container); video_splitter->setStretchFactor(1, 1); - video_splitter->restoreState(settings.video_splitter_state); + video_splitter->restoreState(utils::qbytes(settings.video_splitter_state)); video_splitter->handle(1)->setEnabled(!can->liveStreaming()); video_dock->setWidget(video_splitter); QObject::connect(charts_widget, &ChartsWidget::toggleChartsDocking, this, &MainWindow::toggleChartsDocking); @@ -220,6 +225,14 @@ void MainWindow::undoStackCleanChanged(bool clean) { setWindowModified(!clean); } +void MainWindow::updateUndoRedoActions() { + auto stack = UndoStack::instance(); + undo_act->setEnabled(stack->canUndo()); + undo_act->setText(stack->canUndo() ? tr("&Undo %1").arg(QString::fromStdString(stack->undoText())) : tr("&Undo")); + redo_act->setEnabled(stack->canRedo()); + redo_act->setText(stack->canRedo() ? tr("&Redo %1").arg(QString::fromStdString(stack->redoText())) : tr("&Redo")); +} + void MainWindow::DBCFileChanged() { UndoStack::instance()->clear(); @@ -253,16 +266,16 @@ void MainWindow::selectAndOpenStream() { void MainWindow::closeStream() { openStream(new DummyStream(this)); if (dbc()->nonEmptyDBCCount() > 0) { - emit dbc()->DBCFileChanged(); + emit dbcNotifier()->DBCFileChanged(); } statusBar()->showMessage(tr("stream closed")); } void MainWindow::exportToCSV() { - QString dir = QString("%1/%2.csv").arg(settings.last_dir).arg(QString::fromStdString(can->routeName())); + QString dir = QString("%1/%2.csv").arg(QString::fromStdString(settings.last_dir)).arg(QString::fromStdString(can->routeName())); QString fn = QFileDialog::getSaveFileName(this, "Export stream to CSV file", dir, tr("csv (*.csv)")); if (!fn.isEmpty()) { - utils::exportToCSV(fn); + utils::exportToCSV(fn.toStdString()); } } @@ -273,7 +286,7 @@ void MainWindow::newFile(SourceSet s) { void MainWindow::openFile(SourceSet s) { remindSaveChanges(); - QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)"); + QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), QString::fromStdString(settings.last_dir), "DBC (*.dbc)"); if (!fn.isEmpty()) { loadFile(fn, s); } @@ -283,13 +296,13 @@ void MainWindow::loadFile(const QString &fn, SourceSet s) { if (!fn.isEmpty()) { closeFile(s); - QString error; + std::string error; if (dbc()->open(s, fn.toStdString(), &error)) { updateRecentFiles(fn); statusBar()->showMessage(tr("DBC File %1 loaded").arg(fn), 2000); } else { QMessageBox msg_box(QMessageBox::Warning, tr("Failed to load DBC file"), tr("Failed to parse DBC file %1").arg(fn)); - msg_box.setDetailedText(error); + msg_box.setDetailedText(QString::fromStdString(error)); msg_box.exec(); } } @@ -300,16 +313,25 @@ void MainWindow::loadDBCFromOpendbc(const QString &name) { } void MainWindow::loadFromClipboard(SourceSet s, bool close_all) { + std::string text; + if (!utils::getClipboardText(&text)) { + QMessageBox::warning(this, tr("Load From Clipboard"), tr("No clipboard tool found. Install xclip (X11) or wl-clipboard (Wayland).")); + return; + } + if (text.empty()) { + QMessageBox::warning(this, tr("Load From Clipboard"), tr("Clipboard is empty.")); + return; + } + closeFile(s); - QString dbc_str = QGuiApplication::clipboard()->text(); - QString error; - bool ret = dbc()->open(s, std::string(""), dbc_str.toStdString(), &error); + std::string error; + bool ret = dbc()->open(s, std::string(""), text, &error); if (ret && dbc()->nonEmptyDBCCount() > 0) { QMessageBox::information(this, tr("Load From Clipboard"), tr("DBC Successfully Loaded!")); } else { QMessageBox msg_box(QMessageBox::Warning, tr("Failed to load DBC from clipboard"), tr("Make sure that you paste the text with correct format.")); - msg_box.setDetailedText(error); + msg_box.setDetailedText(QString::fromStdString(error)); msg_box.exec(); } } @@ -373,8 +395,11 @@ void MainWindow::eventsMerged() { .arg(QString::fromStdString(can->routeName())) .arg(car_fingerprint.isEmpty() ? tr("Unknown Car") : car_fingerprint)); // Don't overwrite already loaded DBC - if (!dbc()->nonEmptyDBCCount() && fingerprint_to_dbc.object().contains(car_fingerprint)) { - QTimer::singleShot(0, this, [this]() { loadDBCFromOpendbc(fingerprint_to_dbc[car_fingerprint].toString() + ".dbc"); }); + auto it = fingerprint_to_dbc.find(car_fingerprint.toStdString()); + if (!dbc()->nonEmptyDBCCount() && it != fingerprint_to_dbc.end()) { + QTimer::singleShot(0, this, [this, dbc_name = QString::fromStdString(it->second)]() { + loadDBCFromOpendbc(dbc_name + ".dbc"); + }); } } } @@ -427,7 +452,7 @@ void MainWindow::saveFile(DBCFile *dbc_file) { void MainWindow::saveFileAs(DBCFile *dbc_file) { QString title = tr("Save File (bus: %1)").arg(QString::fromStdString(toString(dbc()->sources(dbc_file)))); - QString fn = QFileDialog::getSaveFileName(this, title, QDir::cleanPath(settings.last_dir + "/untitled.dbc"), tr("DBC (*.dbc)")); + QString fn = QFileDialog::getSaveFileName(this, title, QString::fromStdString((std::filesystem::path(settings.last_dir) / "untitled.dbc").string()), tr("DBC (*.dbc)")); if (!fn.isEmpty()) { dbc_file->saveAs(fn.toStdString()); UndoStack::instance()->setClean(); @@ -446,8 +471,11 @@ void MainWindow::saveToClipboard() { void MainWindow::saveFileToClipboard(DBCFile *dbc_file) { assert(dbc_file != nullptr); - QGuiApplication::clipboard()->setText(QString::fromStdString(dbc_file->generateDBC())); - QMessageBox::information(this, tr("Copy To Clipboard"), tr("DBC Successfully copied!")); + if (utils::setClipboardText(dbc_file->generateDBC())) { + QMessageBox::information(this, tr("Copy To Clipboard"), tr("DBC Successfully copied!")); + } else { + QMessageBox::warning(this, tr("Copy To Clipboard"), tr("Failed to copy DBC to clipboard. Install xclip (X11) or wl-clipboard (Wayland).")); + } } void MainWindow::updateLoadSaveMenus() { @@ -481,12 +509,13 @@ void MainWindow::updateLoadSaveMenus() { } void MainWindow::updateRecentFiles(const QString &fn) { - settings.recent_files.removeAll(fn); - settings.recent_files.prepend(fn); + const std::string filename = fn.toStdString(); + settings.recent_files.erase(std::remove(settings.recent_files.begin(), settings.recent_files.end(), filename), settings.recent_files.end()); + settings.recent_files.insert(settings.recent_files.begin(), filename); while (settings.recent_files.size() > MAX_RECENT_FILES) { - settings.recent_files.removeLast(); + settings.recent_files.pop_back(); } - settings.last_dir = QFileInfo(fn).absolutePath(); + settings.last_dir = std::filesystem::absolute(fn.toStdString()).parent_path().string(); } void MainWindow::updateRecentFileMenu() { @@ -499,8 +528,8 @@ void MainWindow::updateRecentFileMenu() { } for (int i = 0; i < num_recent_files; ++i) { - QString text = tr("&%1 %2").arg(i + 1).arg(QFileInfo(settings.recent_files[i]).fileName()); - open_recent_menu->addAction(text, this, [this, file = settings.recent_files[i]]() { loadFile(file); }); + QString text = tr("&%1 %2").arg(i + 1).arg(QString::fromStdString(std::filesystem::path(settings.recent_files[i]).filename().string())); + open_recent_menu->addAction(text, this, [this, file = settings.recent_files[i]]() { loadFile(QString::fromStdString(file)); }); } } @@ -560,22 +589,23 @@ void MainWindow::closeEvent(QCloseEvent *event) { remindSaveChanges(); installDownloadProgressHandler(nullptr); - qInstallMessageHandler(nullptr); + installMessageHandler(nullptr); if (floating_window) floating_window->deleteLater(); // save states - settings.geometry = saveGeometry(); - settings.window_state = saveState(); + settings.geometry = utils::toBytes(saveGeometry()); + settings.window_state = utils::toBytes(saveState()); if (can && !can->liveStreaming()) { - settings.video_splitter_state = video_splitter->saveState(); + settings.video_splitter_state = utils::toBytes(video_splitter->saveState()); } if (messages_widget) { settings.message_header_state = messages_widget->saveHeaderState(); } saveSessionState(); + settings.save(); QWidget::closeEvent(event); } @@ -627,30 +657,39 @@ void MainWindow::saveSessionState() { settings.active_charts.clear(); for (auto &f : dbc()->allDBCFiles()) - if (!f->isEmpty()) { settings.recent_dbc_file = QString::fromStdString(f->filename); break; } + if (!f->isEmpty()) { settings.recent_dbc_file = f->filename; break; } if (auto *detail = center_widget->getDetailWidget()) { auto [active_id, ids] = detail->serializeMessageIds(); - settings.active_msg_id = active_id; - settings.selected_msg_ids = ids; + settings.active_msg_id = active_id.toStdString(); + settings.selected_msg_ids.clear(); + for (const auto &id : ids) settings.selected_msg_ids.push_back(id.toStdString()); + } + if (charts_widget) { + settings.active_charts.clear(); + for (const auto &id : charts_widget->serializeChartIds()) settings.active_charts.push_back(id.toStdString()); } - if (charts_widget) - settings.active_charts = charts_widget->serializeChartIds(); } void MainWindow::restoreSessionState() { - if (settings.recent_dbc_file.isEmpty() || dbc()->nonEmptyDBCCount() == 0) return; + if (settings.recent_dbc_file.empty() || dbc()->nonEmptyDBCCount() == 0) return; QString dbc_file; for (auto& f : dbc()->allDBCFiles()) if (!f->isEmpty()) { dbc_file = QString::fromStdString(f->filename); break; } - if (dbc_file != settings.recent_dbc_file) return; + if (dbc_file.toStdString() != settings.recent_dbc_file) return; - if (!settings.selected_msg_ids.isEmpty()) - center_widget->ensureDetailWidget()->restoreTabs(settings.active_msg_id, settings.selected_msg_ids); + if (!settings.selected_msg_ids.empty()) { + QStringList ids; + for (const auto &id : settings.selected_msg_ids) ids.push_back(QString::fromStdString(id)); + center_widget->ensureDetailWidget()->restoreTabs(QString::fromStdString(settings.active_msg_id), ids); + } - if (charts_widget != nullptr && !settings.active_charts.empty()) - charts_widget->restoreChartsFromIds(settings.active_charts); + if (charts_widget != nullptr && !settings.active_charts.empty()) { + QStringList ids; + for (const auto &id : settings.active_charts) ids.push_back(QString::fromStdString(id)); + charts_widget->restoreChartsFromIds(ids); + } } // HelpOverlay diff --git a/tools/cabana/mainwin.h b/openpilot/tools/cabana/mainwin.h similarity index 92% rename from tools/cabana/mainwin.h rename to openpilot/tools/cabana/mainwin.h index 92c2714ae7..279ea3b969 100644 --- a/tools/cabana/mainwin.h +++ b/openpilot/tools/cabana/mainwin.h @@ -1,13 +1,16 @@ #pragma once #include -#include #include #include #include #include #include +#include #include +#include +#include +#include #include "tools/cabana/chart/chartswidget.h" #include "tools/cabana/dbc/dbcmanager.h" @@ -67,6 +70,7 @@ protected: void findSimilarBits(); void findSignal(); void undoStackCleanChanged(bool clean); + void updateUndoRedoActions(); void onlineHelp(); void toggleFullScreen(); void updateStatus(); @@ -85,7 +89,7 @@ protected: QVBoxLayout *charts_layout; QProgressBar *progress_bar; QLabel *status_label; - QJsonDocument fingerprint_to_dbc; + std::unordered_map fingerprint_to_dbc; QSplitter *video_splitter = nullptr; enum { MAX_RECENT_FILES = 15 }; QMenu *open_recent_menu = nullptr; @@ -96,8 +100,10 @@ protected: QAction *save_dbc = nullptr; QAction *save_dbc_as = nullptr; QAction *copy_dbc_to_clipboard = nullptr; + QAction *undo_act = nullptr; + QAction *redo_act = nullptr; QString car_fingerprint; - QByteArray default_state; + std::vector default_state; }; class HelpOverlay : public QWidget { diff --git a/tools/cabana/messageswidget.cc b/openpilot/tools/cabana/messageswidget.cc similarity index 90% rename from tools/cabana/messageswidget.cc rename to openpilot/tools/cabana/messageswidget.cc index 75cdaa7cc3..b07e3ea0bf 100644 --- a/tools/cabana/messageswidget.cc +++ b/openpilot/tools/cabana/messageswidget.cc @@ -1,4 +1,5 @@ #include "tools/cabana/messageswidget.h" +#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -43,8 +44,8 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget QObject::connect(header, &MessageViewHeader::customContextMenuRequested, this, &MessagesWidget::headerContextMenuEvent); QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions); QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, model, &MessageListModel::dbcModified); - QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, model, &MessageListModel::dbcModified); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &MessageListModel::dbcModified); + QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, model, &MessageListModel::dbcModified); QObject::connect(model, &MessageListModel::modelReset, [this]() { if (current_msg_id) { selectMessage(*current_msg_id); @@ -211,7 +212,7 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { return {}; } -void MessageListModel::setFilterStrings(const QMap &filters) { +void MessageListModel::setFilterStrings(const std::map &filters) { filters_ = filters; filterAndSort(); } @@ -264,14 +265,14 @@ static bool parseRange(const QString &filter, uint32_t value, int base = 10) { } bool MessageListModel::match(const MessageListModel::Item &item) { - if (filters_.isEmpty()) + if (filters_.empty()) return true; bool match = true; const auto &data = can->lastMessage(item.id); for (auto it = filters_.cbegin(); it != filters_.cend() && match; ++it) { - const QString &txt = it.value(); - switch (it.key()) { + const QString &txt = it->second; + switch (it->first) { case Column::NAME: { match = item.name.contains(txt, Qt::CaseInsensitive); if (!match) { @@ -387,10 +388,13 @@ void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, painter->setPen(oldPen); } -void MessageView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { +void MessageView::setModel(QAbstractItemModel *model) { + QTreeView::setModel(model); // Bypass the slow call to QTreeView::dataChanged. // QTreeView::dataChanged will invalidate the height cache and that's what we don't need in MessageView. - QAbstractItemView::dataChanged(topLeft, bottomRight, roles); + QObject::disconnect(model, &QAbstractItemModel::dataChanged, this, nullptr); + QObject::connect(model, &QAbstractItemModel::dataChanged, this, + [this](const QModelIndex &tl, const QModelIndex &br, const auto &roles) { QAbstractItemView::dataChanged(tl, br, roles); }); } void MessageView::updateBytesSectionSize() { @@ -421,9 +425,9 @@ MessageViewHeader::MessageViewHeader(QWidget *parent) : QHeaderView(Qt::Horizont } void MessageViewHeader::updateFilters() { - QMap filters; - for (int i = 0; i < count(); i++) { - if (editors[i] && !editors[i]->text().isEmpty()) { + std::map filters; + for (int i = 0; i < (int)editors.size(); i++) { + if (!editors[i]->text().isEmpty()) { filters[i] = editors[i]->text(); } } @@ -432,27 +436,24 @@ void MessageViewHeader::updateFilters() { void MessageViewHeader::updateHeaderPositions() { QSize sz = QHeaderView::sizeHint(); - for (int i = 0; i < count(); i++) { - if (editors[i]) { - int h = editors[i]->sizeHint().height(); - editors[i]->setGeometry(sectionViewportPosition(i), sz.height(), sectionSize(i), h); - editors[i]->setHidden(isSectionHidden(i)); - } + for (int i = 0; i < (int)editors.size(); i++) { + int h = editors[i]->sizeHint().height(); + editors[i]->setGeometry(sectionViewportPosition(i), sz.height(), sectionSize(i), h); + editors[i]->setHidden(isSectionHidden(i)); } } void MessageViewHeader::updateGeometries() { - for (int i = 0; i < count(); i++) { - if (!editors[i]) { - QString column_name = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); - editors[i] = new QLineEdit(this); - editors[i]->setClearButtonEnabled(true); - editors[i]->setPlaceholderText(tr("Filter %1").arg(column_name)); + for (int i = (int)editors.size(); i < count(); i++) { + QString column_name = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); + auto edit = new QLineEdit(this); + edit->setClearButtonEnabled(true); + edit->setPlaceholderText(tr("Filter %1").arg(column_name)); - QObject::connect(editors[i], &QLineEdit::textChanged, this, &MessageViewHeader::updateFilters); - } + QObject::connect(edit, &QLineEdit::textChanged, this, &MessageViewHeader::updateFilters); + editors.push_back(edit); } - setViewportMargins(0, 0, 0, editors[0] ? editors[0]->sizeHint().height() : 0); + setViewportMargins(0, 0, 0, !editors.empty() ? editors[0]->sizeHint().height() : 0); QHeaderView::updateGeometries(); updateHeaderPositions(); @@ -460,5 +461,5 @@ void MessageViewHeader::updateGeometries() { QSize MessageViewHeader::sizeHint() const { QSize sz = QHeaderView::sizeHint(); - return editors[0] ? QSize(sz.width(), sz.height() + editors[0]->height() + 1) : sz; + return !editors.empty() ? QSize(sz.width(), sz.height() + editors[0]->height() + 1) : sz; } diff --git a/tools/cabana/messageswidget.h b/openpilot/tools/cabana/messageswidget.h similarity index 85% rename from tools/cabana/messageswidget.h rename to openpilot/tools/cabana/messageswidget.h index 9ffb156604..0a9cd256d8 100644 --- a/tools/cabana/messageswidget.h +++ b/openpilot/tools/cabana/messageswidget.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include #include @@ -35,7 +37,7 @@ public: QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; int rowCount(const QModelIndex &parent = QModelIndex()) const override { return items_.size(); } void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; - void setFilterStrings(const QMap &filters); + void setFilterStrings(const std::map &filters); void showInactiveMessages(bool show); void msgsReceived(const std::set *new_msgs, bool has_new_ids); bool filterAndSort(); @@ -56,7 +58,7 @@ private: void sortItems(std::vector &items); bool match(const MessageListModel::Item &id); - QMap filters_; + std::map filters_; std::set dbc_messages_; int sort_column = 0; Qt::SortOrder sort_order = Qt::AscendingOrder; @@ -68,11 +70,11 @@ class MessageView : public QTreeView { public: MessageView(QWidget *parent) : QTreeView(parent) {} void updateBytesSectionSize(); + void setModel(QAbstractItemModel *model) override; protected: void drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; void drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const override {} - void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()) override; void wheelEvent(QWheelEvent *event) override; }; @@ -86,7 +88,7 @@ public: QSize sizeHint() const override; void updateFilters(); - QMap editors; + std::vector editors; }; class MessagesWidget : public QWidget { @@ -95,8 +97,13 @@ class MessagesWidget : public QWidget { public: MessagesWidget(QWidget *parent); void selectMessage(const MessageId &message_id); - QByteArray saveHeaderState() const { return view->header()->saveState(); } - bool restoreHeaderState(const QByteArray &state) const { return view->header()->restoreState(state); } + std::vector saveHeaderState() const { + const auto state = view->header()->saveState(); + return {state.begin(), state.end()}; + } + bool restoreHeaderState(const std::vector &state) const { + return view->header()->restoreState({(const char *)state.data(), (int)state.size()}); + } void suppressHighlighted(); signals: diff --git a/tools/cabana/panda.cc b/openpilot/tools/cabana/panda.cc similarity index 99% rename from tools/cabana/panda.cc rename to openpilot/tools/cabana/panda.cc index cf5354a507..33117812d3 100644 --- a/tools/cabana/panda.cc +++ b/openpilot/tools/cabana/panda.cc @@ -6,7 +6,7 @@ #include #include -#include "cereal/messaging/messaging.h" +#include "openpilot/cereal/messaging/messaging.h" #include "common/swaglog.h" #include "common/util.h" diff --git a/tools/cabana/panda.h b/openpilot/tools/cabana/panda.h similarity index 96% rename from tools/cabana/panda.h rename to openpilot/tools/cabana/panda.h index 8b861a2476..216a6b72d7 100644 --- a/tools/cabana/panda.h +++ b/openpilot/tools/cabana/panda.h @@ -13,8 +13,8 @@ #include #include -#include "cereal/gen/cpp/car.capnp.h" -#include "cereal/gen/cpp/log.capnp.h" +#include "openpilot/cereal/gen/cpp/car.capnp.h" +#include "openpilot/cereal/gen/cpp/log.capnp.h" #include "panda/board/health.h" #include "panda/board/can.h" diff --git a/openpilot/tools/cabana/settings.cc b/openpilot/tools/cabana/settings.cc new file mode 100644 index 0000000000..0c8136b84c --- /dev/null +++ b/openpilot/tools/cabana/settings.cc @@ -0,0 +1,618 @@ +#include "tools/cabana/settings.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifdef __APPLE__ +#include +#endif + +#include +#include +#include +#include +#include +#include + +#include "json11/json11.hpp" +#include "tools/cabana/utils/util.h" + +const int MIN_CACHE_MINIUTES = 30; +const int MAX_CACHE_MINIUTES = 120; + +Settings settings; + +namespace { + +std::filesystem::path settingsFile() { + return utils::configPath() / "cabana.json"; +} + +struct LoadedSettings { + json11::Json::object values; + bool exists = false; + bool valid = true; +}; + +class FileLock { +public: + explicit FileLock(const std::filesystem::path &path) { + fd = open(path.c_str(), O_CREAT | O_CLOEXEC, 0600); + if (fd < 0 || flock(fd, LOCK_EX) < 0) { + fprintf(stderr, "failed to lock Cabana settings %s: %s\n", path.c_str(), strerror(errno)); + if (fd >= 0) close(fd); + fd = -1; + } + } + ~FileLock() { + if (fd >= 0) close(fd); + } + bool isLocked() const { return fd >= 0; } + +private: + int fd = -1; +}; + +LoadedSettings loadSettings() { + std::ifstream input(settingsFile()); + if (!input) return {}; + + const std::string contents{std::istreambuf_iterator(input), std::istreambuf_iterator()}; + std::string error; + auto settings_json = json11::Json::parse(contents, error); + if (!error.empty() || !settings_json.is_object()) { + fprintf(stderr, "failed to read Cabana settings %s%s%s\n", settingsFile().c_str(), error.empty() ? "" : ": ", error.c_str()); + return {.exists = true, .valid = false}; + } + return {.values = settings_json.object_items(), .exists = true}; +} + +bool ensureSettingsDirectory() { + const auto path = settingsFile(); + std::error_code error; + std::filesystem::create_directories(path.parent_path(), error); + if (error) { + fprintf(stderr, "failed to create Cabana settings directory %s: %s\n", path.parent_path().c_str(), error.message().c_str()); + return false; + } + return true; +} + +bool writeAll(int fd, const std::string &data) { + size_t written = 0; + while (written < data.size()) { + ssize_t result = write(fd, data.data() + written, data.size() - written); + if (result < 0 && errno == EINTR) continue; + if (result <= 0) return false; + written += result; + } + return true; +} + +bool saveSettings(const json11::Json::object &settings_json) { + const auto path = settingsFile(); + const std::string contents = json11::Json(settings_json).dump(); + std::string temporary_path = path.string() + ".tmp.XXXXXX"; + int fd = mkstemp(temporary_path.data()); + if (fd < 0) { + fprintf(stderr, "failed to create temporary Cabana settings %s: %s\n", temporary_path.c_str(), strerror(errno)); + return false; + } + + bool success = writeAll(fd, contents) && fsync(fd) == 0; + if (close(fd) < 0) success = false; + if (success && rename(temporary_path.c_str(), path.c_str()) < 0) success = false; + + if (success) { + int dir_fd = open(path.parent_path().c_str(), O_RDONLY | O_CLOEXEC); + success = dir_fd >= 0 && fsync(dir_fd) == 0; + if (dir_fd >= 0 && close(dir_fd) < 0) success = false; + } + + if (!success) { + const int saved_errno = errno; + unlink(temporary_path.c_str()); + fprintf(stderr, "failed to save Cabana settings to %s: %s\n", path.c_str(), strerror(saved_errno)); + } + return success; +} + +bool preserveCorruptSettings() { + const auto path = settingsFile(); + auto backup = path; + backup += ".corrupt"; + for (int i = 1; std::filesystem::exists(backup); ++i) { + backup = path; + backup += ".corrupt." + std::to_string(i); + } + if (rename(path.c_str(), backup.c_str()) < 0) { + fprintf(stderr, "failed to preserve corrupt Cabana settings %s: %s\n", path.c_str(), strerror(errno)); + return false; + } + fprintf(stderr, "preserved corrupt Cabana settings at %s\n", backup.c_str()); + return true; +} + +// TODO: Remove the legacy QSettings migration after users have had time to migrate to cabana.json. +struct LegacyValue { + std::vector strings; + std::string bytes; + bool is_byte_array = false; +}; + +using LegacySettings = std::map; + +int hexDigit(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +#ifndef __APPLE__ + +void appendUtf8(std::string &result, uint32_t codepoint) { + if (codepoint <= 0x7f) { + result.push_back(codepoint); + } else if (codepoint <= 0x7ff) { + result.push_back(0xc0 | (codepoint >> 6)); + result.push_back(0x80 | (codepoint & 0x3f)); + } else if (codepoint <= 0xffff) { + result.push_back(0xe0 | (codepoint >> 12)); + result.push_back(0x80 | ((codepoint >> 6) & 0x3f)); + result.push_back(0x80 | (codepoint & 0x3f)); + } else { + result.push_back(0xf0 | (codepoint >> 18)); + result.push_back(0x80 | ((codepoint >> 12) & 0x3f)); + result.push_back(0x80 | ((codepoint >> 6) & 0x3f)); + result.push_back(0x80 | (codepoint & 0x3f)); + } +} + +LegacyValue decodeIniValue(std::string_view encoded) { + std::vector> decoded(1); + std::vector quoted(1, false); + bool in_quotes = false; + + for (size_t i = 0; i < encoded.size();) { + char c = encoded[i++]; + if (c == '"') { + in_quotes = !in_quotes; + quoted.back() = true; + } else if (c == ',' && !in_quotes) { + decoded.emplace_back(); + quoted.push_back(false); + while (i < encoded.size() && (encoded[i] == ' ' || encoded[i] == '\t')) ++i; + } else if (c == '\\' && i < encoded.size()) { + c = encoded[i++]; + static const std::map escapes = { + {'a', '\a'}, {'b', '\b'}, {'f', '\f'}, {'n', '\n'}, {'r', '\r'}, {'t', '\t'}, + {'v', '\v'}, {'"', '"'}, {'?', '?'}, {'\'', '\''}, {'\\', '\\'}, + }; + if (auto it = escapes.find(c); it != escapes.end()) { + decoded.back().push_back(static_cast(it->second)); + } else if (c == 'x' && i < encoded.size() && hexDigit(encoded[i]) >= 0) { + uint32_t value = 0; + while (i < encoded.size() && hexDigit(encoded[i]) >= 0) value = (value << 4) + hexDigit(encoded[i++]); + decoded.back().push_back(value & 0xffff); + } else if (c >= '0' && c <= '7') { + uint32_t value = c - '0'; + while (i < encoded.size() && encoded[i] >= '0' && encoded[i] <= '7') value = (value << 3) + (encoded[i++] - '0'); + decoded.back().push_back(value & 0xffff); + } + } else { + decoded.back().push_back(static_cast(c)); + } + } + + LegacyValue result; + for (size_t i = 0; i < decoded.size(); ++i) { + auto &value = decoded[i]; + if (!quoted[i]) { + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) value.erase(value.begin()); + while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) value.pop_back(); + } + + std::string string_value; + for (size_t j = 0; j < value.size(); ++j) { + uint32_t codepoint = value[j]; + if (codepoint >= 0xd800 && codepoint <= 0xdbff && j + 1 < value.size() && value[j + 1] >= 0xdc00 && value[j + 1] <= 0xdfff) { + codepoint = 0x10000 + ((codepoint - 0xd800) << 10) + (value[++j] - 0xdc00); + } + appendUtf8(string_value, codepoint); + } + result.strings.push_back(std::move(string_value)); + } + + if (result.strings.size() == 1 && result.strings[0] == "@Invalid()") { + result.strings.clear(); + } else if (decoded.size() == 1) { + static constexpr std::string_view prefix = "@ByteArray("; + const auto &value = decoded[0]; + if (value.size() >= prefix.size() + 1 && std::equal(prefix.begin(), prefix.end(), value.begin()) && value.back() == ')') { + result.is_byte_array = true; + result.bytes.reserve(value.size() - prefix.size() - 1); + for (size_t i = prefix.size(); i + 1 < value.size(); ++i) result.bytes.push_back(value[i] & 0xff); + } + } + if (!result.is_byte_array) { + for (auto &value : result.strings) { + if (value.compare(0, 2, "@@") == 0) value.erase(0, 1); + } + } + return result; +} + +LegacySettings loadLegacySettings() { + auto path = settingsFile(); + path.replace_filename("cabana.conf"); + std::ifstream input(path); + if (!input) return {}; + + LegacySettings settings; + bool in_general_section = false; + std::string line; + while (std::getline(input, line)) { + if (!line.empty() && line.back() == '\r') line.pop_back(); + if (line == "[General]") { + in_general_section = true; + continue; + } + if (!line.empty() && line.front() == '[') { + in_general_section = false; + continue; + } + if (!in_general_section || line.empty() || line.front() == ';') continue; + if (auto separator = line.find('='); separator != std::string::npos) { + settings[line.substr(0, separator)] = decodeIniValue(std::string_view(line).substr(separator + 1)); + } + } + return settings; +} + +#else + +std::string cfStringToUtf8(CFStringRef value) { + CFIndex length = CFStringGetLength(value); + CFIndex size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; + std::string result(size, '\0'); + if (!CFStringGetCString(value, result.data(), size, kCFStringEncodingUTF8)) return {}; + result.resize(strlen(result.c_str())); + return result; +} + +LegacyValue cfStringValue(CFStringRef string) { + LegacyValue value; + if (CFStringHasPrefix(string, CFSTR("@ByteArray(")) && CFStringHasSuffix(string, CFSTR(")"))) { + CFRange range{11, CFStringGetLength(string) - 12}; + std::vector data(range.length); + CFStringGetCharacters(string, range, data.data()); + value.is_byte_array = true; + value.bytes.reserve(data.size()); + for (UniChar c : data) value.bytes.push_back(c & 0xff); + } else { + std::string string_value = cfStringToUtf8(string); + if (string_value.compare(0, 2, "@@") == 0) string_value.erase(0, 1); + value.strings.push_back(std::move(string_value)); + } + return value; +} + +LegacySettings loadLegacySettings() { + LegacySettings settings; + CFDictionaryRef values = CFPreferencesCopyMultiple(nullptr, CFSTR("com.cabana"), + kCFPreferencesCurrentUser, kCFPreferencesAnyHost); + if (values == nullptr) return settings; + + CFIndex count = CFDictionaryGetCount(values); + std::vector keys(count); + std::vector objects(count); + CFDictionaryGetKeysAndValues(values, keys.data(), objects.data()); + for (CFIndex i = 0; i < count; ++i) { + if (CFGetTypeID(keys[i]) != CFStringGetTypeID()) continue; + std::string key = cfStringToUtf8(static_cast(keys[i])); + CFTypeRef object = objects[i]; + LegacyValue value; + if (CFGetTypeID(object) == CFBooleanGetTypeID()) { + value.strings.push_back(CFBooleanGetValue(static_cast(object)) ? "true" : "false"); + } else if (CFGetTypeID(object) == CFNumberGetTypeID()) { + int number = 0; + if (CFNumberGetValue(static_cast(object), kCFNumberIntType, &number)) value.strings.push_back(std::to_string(number)); + } else if (CFGetTypeID(object) == CFStringGetTypeID()) { + value = cfStringValue(static_cast(object)); + } else if (CFGetTypeID(object) == CFDataGetTypeID()) { + auto data = static_cast(object); + value.is_byte_array = true; + value.bytes.assign(reinterpret_cast(CFDataGetBytePtr(data)), CFDataGetLength(data)); + } else if (CFGetTypeID(object) == CFArrayGetTypeID()) { + auto array = static_cast(object); + for (CFIndex j = 0; j < CFArrayGetCount(array); ++j) { + CFTypeRef item = CFArrayGetValueAtIndex(array, j); + if (CFGetTypeID(item) != CFStringGetTypeID()) { + value.strings.clear(); + break; + } + auto item_value = cfStringValue(static_cast(item)); + if (item_value.strings.size() != 1) { + value.strings.clear(); + break; + } + value.strings.push_back(std::move(item_value.strings[0])); + } + } + if (!value.strings.empty() || value.is_byte_array || CFGetTypeID(object) == CFArrayGetTypeID()) { + settings.emplace(std::move(key), std::move(value)); + } + } + CFRelease(values); + return settings; +} + +#endif + +template +void readLegacySetting(const LegacySettings &legacy_settings, const char *key, T &value) { + auto it = legacy_settings.find(key); + if (it == legacy_settings.end() || it->second.strings.size() != 1) return; + const auto &stored = it->second.strings[0]; + + if constexpr (std::is_same_v) { + if (stored == "true") value = true; + if (stored == "false") value = false; + } else if constexpr (std::is_integral_v || std::is_enum_v) { + int number = 0; + auto [end, error] = std::from_chars(stored.data(), stored.data() + stored.size(), number); + if (error == std::errc{} && end == stored.data() + stored.size()) value = static_cast(number); + } +} + +void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::string &value) { + auto it = legacy_settings.find(key); + if (it != legacy_settings.end() && it->second.strings.size() == 1) value = it->second.strings[0]; +} + +void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::vector &value) { + auto it = legacy_settings.find(key); + if (it != legacy_settings.end() && !it->second.is_byte_array) value = it->second.strings; +} + +void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::vector &value) { + auto it = legacy_settings.find(key); + if (it != legacy_settings.end() && it->second.is_byte_array) { + value.assign(it->second.bytes.begin(), it->second.bytes.end()); + } +} + +template +void readSetting(const json11::Json::object &settings_json, const char *key, T &value) { + auto it = settings_json.find(key); + if (it == settings_json.end()) return; + + if constexpr (std::is_same_v) { + if (it->second.is_bool()) value = it->second.bool_value(); + } else if constexpr (std::is_integral_v) { + if (it->second.is_number()) value = it->second.int_value(); + } else if constexpr (std::is_enum_v) { + if (it->second.is_number()) value = static_cast(it->second.int_value()); + } +} + +void readSetting(const json11::Json::object &settings_json, const char *key, std::string &value) { + auto it = settings_json.find(key); + if (it != settings_json.end() && it->second.is_string()) value = it->second.string_value(); +} + +void readSetting(const json11::Json::object &settings_json, const char *key, std::vector &value) { + auto it = settings_json.find(key); + if (it == settings_json.end() || !it->second.is_array()) return; + + std::vector stored; + for (const auto &item : it->second.array_items()) { + if (!item.is_string()) return; + stored.push_back(item.string_value()); + } + value = std::move(stored); +} + +void readSetting(const json11::Json::object &settings_json, const char *key, std::vector &value) { + auto it = settings_json.find(key); + if (it == settings_json.end() || !it->second.is_string()) return; + + const auto &hex = it->second.string_value(); + if (hex.size() % 2 == 0 && std::all_of(hex.begin(), hex.end(), [](unsigned char c) { return std::isxdigit(c); })) { + value.clear(); + value.reserve(hex.size() / 2); + for (size_t i = 0; i < hex.size(); i += 2) { + value.push_back((hexDigit(hex[i]) << 4) | hexDigit(hex[i + 1])); + } + } +} + +template +void writeSetting(json11::Json::object &settings_json, const char *key, const T &value) { + if constexpr (std::is_same_v) { + settings_json[key] = value; + } else if constexpr (std::is_integral_v || std::is_enum_v) { + settings_json[key] = static_cast(value); + } +} + +void writeSetting(json11::Json::object &settings_json, const char *key, const std::string &value) { + settings_json[key] = value; +} + +void writeSetting(json11::Json::object &settings_json, const char *key, const std::vector &value) { + settings_json[key] = value; +} + +void writeSetting(json11::Json::object &settings_json, const char *key, const std::vector &value) { + static const char digits[] = "0123456789abcdef"; + std::string hex; + hex.reserve(value.size() * 2); + for (uint8_t b : value) { + hex.push_back(digits[b >> 4]); + hex.push_back(digits[b & 0xf]); + } + settings_json[key] = hex; +} + +template +void settingsOp(Store &s, SettingOperation op) { + op(s, "absolute_time", settings.absolute_time); + op(s, "fps", settings.fps); + op(s, "max_cached_minutes", settings.max_cached_minutes); + op(s, "chart_height", settings.chart_height); + op(s, "chart_range", settings.chart_range); + op(s, "chart_column_count", settings.chart_column_count); + op(s, "last_dir", settings.last_dir); + op(s, "last_route_dir", settings.last_route_dir); + op(s, "window_state", settings.window_state); + op(s, "geometry", settings.geometry); + op(s, "video_splitter_state", settings.video_splitter_state); + op(s, "recent_files", settings.recent_files); + op(s, "message_header_state", settings.message_header_state); + op(s, "chart_series_type", settings.chart_series_type); + op(s, "theme", settings.theme); + op(s, "sparkline_range", settings.sparkline_range); + op(s, "multiple_lines_hex", settings.multiple_lines_hex); + op(s, "log_livestream", settings.log_livestream); + op(s, "log_path", settings.log_path); + op(s, "drag_direction", (int &)settings.drag_direction); + op(s, "suppress_defined_signals", settings.suppress_defined_signals); + op(s, "recent_dbc_file", settings.recent_dbc_file); + op(s, "active_msg_id", settings.active_msg_id); + op(s, "selected_msg_ids", settings.selected_msg_ids); + op(s, "active_charts", settings.active_charts); +} + +} // namespace + +Settings::Settings() { + last_dir = last_route_dir = utils::homePath(); + log_path = utils::homePath() + "/cabana_live_stream/"; + const auto stored_settings = loadSettings(); + if (stored_settings.valid) { + if (stored_settings.exists) { + settingsOp(stored_settings.values, [](const auto &s, const char *key, auto &value) { readSetting(s, key, value); }); + } else { + auto legacy_settings = loadLegacySettings(); + settingsOp(legacy_settings, [](const auto &s, const char *key, auto &value) { readLegacySetting(s, key, value); }); + } + } + fps = std::clamp(fps, 1, 100); +} + +// Must be called before main() returns: json11's internal statistics are constructed on first +// use at runtime, so they are destroyed before this pre-main global. Saving from ~Settings +// would use them after destruction and corrupt the heap. +void Settings::save() { + if (!ensureSettingsDirectory()) return; + + auto lock_path = settingsFile(); + lock_path += ".lock"; + FileLock lock(lock_path); + if (!lock.isLocked()) return; + + auto stored_settings = loadSettings(); + if (!stored_settings.valid && !preserveCorruptSettings()) return; + settingsOp(stored_settings.values, [](auto &s, const char *key, const auto &value) { writeSetting(s, key, value); }); + saveSettings(stored_settings.values); +} + +// SettingsDlg + +SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { + setWindowTitle(tr("Settings")); + QVBoxLayout *main_layout = new QVBoxLayout(this); + QGroupBox *groupbox = new QGroupBox("General"); + QFormLayout *form_layout = new QFormLayout(groupbox); + + form_layout->addRow(tr("Color Theme"), theme = new QComboBox(this)); + theme->setToolTip(tr("You may need to restart cabana after changes theme")); + theme->addItems({tr("Automatic"), tr("Light"), tr("Dark")}); + theme->setCurrentIndex(settings.theme); + + form_layout->addRow("FPS", fps = new QSpinBox(this)); + fps->setRange(10, 100); + fps->setSingleStep(10); + fps->setValue(settings.fps); + + form_layout->addRow(tr("Max Cached Minutes"), cached_minutes = new QSpinBox(this)); + cached_minutes->setRange(MIN_CACHE_MINIUTES, MAX_CACHE_MINIUTES); + cached_minutes->setSingleStep(1); + cached_minutes->setValue(settings.max_cached_minutes); + main_layout->addWidget(groupbox); + + groupbox = new QGroupBox("New Signal Settings"); + form_layout = new QFormLayout(groupbox); + form_layout->addRow(tr("Drag Direction"), drag_direction = new QComboBox(this)); + drag_direction->addItems({tr("MSB First"), tr("LSB First"), tr("Always Little Endian"), tr("Always Big Endian")}); + drag_direction->setCurrentIndex(settings.drag_direction); + main_layout->addWidget(groupbox); + + groupbox = new QGroupBox("Chart"); + form_layout = new QFormLayout(groupbox); + form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this)); + chart_height->setRange(100, 500); + chart_height->setSingleStep(10); + chart_height->setValue(settings.chart_height); + main_layout->addWidget(groupbox); + + log_livestream = new QGroupBox(tr("Enable live stream logging"), this); + log_livestream->setCheckable(true); + log_livestream->setChecked(settings.log_livestream); + QHBoxLayout *path_layout = new QHBoxLayout(log_livestream); + path_layout->addWidget(log_path = new QLineEdit(QString::fromStdString(settings.log_path), this)); + log_path->setReadOnly(true); + auto browse_btn = new QPushButton(tr("B&rowse...")); + path_layout->addWidget(browse_btn); + main_layout->addWidget(log_livestream); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + main_layout->addWidget(buttonBox); + setFixedSize(400, sizeHint().height()); + + QObject::connect(browse_btn, &QPushButton::clicked, [this]() { + QString fn = QFileDialog::getExistingDirectory( + this, tr("Log File Location"), + QString::fromStdString(utils::homePath()), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + if (!fn.isEmpty()) { + log_path->setText(fn); + } + }); + QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDlg::save); +} + +void SettingsDlg::save() { + if (std::exchange(settings.theme, theme->currentIndex()) != settings.theme) { + // set theme before emit changed + utils::setTheme(settings.theme); + } + settings.fps = fps->value(); + settings.max_cached_minutes = cached_minutes->value(); + settings.chart_height = chart_height->value(); + settings.log_livestream = log_livestream->isChecked(); + settings.log_path = log_path->text().toStdString(); + settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex(); + emit settings.changed(); + QDialog::accept(); +} diff --git a/openpilot/tools/cabana/settings.h b/openpilot/tools/cabana/settings.h new file mode 100644 index 0000000000..7357ecf4fc --- /dev/null +++ b/openpilot/tools/cabana/settings.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +#include "tools/cabana/core/settings.h" + +class Settings : public QObject, public CabanaSettingsState { + Q_OBJECT + +public: + Settings(); + void save(); + + // Qt frontend layout state. This intentionally stays outside CabanaSettingsState. + std::vector geometry; + std::vector video_splitter_state; + std::vector window_state; + std::vector message_header_state; + +signals: + void changed(); +}; + +class SettingsDlg : public QDialog { +public: + SettingsDlg(QWidget *parent); + void save(); + QSpinBox *fps; + QSpinBox *cached_minutes; + QSpinBox *chart_height; + QComboBox *chart_series_type; + QComboBox *theme; + QGroupBox *log_livestream; + QLineEdit *log_path; + QComboBox *drag_direction; +}; + +extern Settings settings; diff --git a/tools/cabana/signalview.cc b/openpilot/tools/cabana/signalview.cc similarity index 95% rename from tools/cabana/signalview.cc rename to openpilot/tools/cabana/signalview.cc index a204512e83..ebb5374140 100644 --- a/tools/cabana/signalview.cc +++ b/openpilot/tools/cabana/signalview.cc @@ -1,9 +1,9 @@ #include "tools/cabana/signalview.h" +#include "tools/cabana/dbc/dbcqt.h" #include #include -#include #include #include #include @@ -15,6 +15,7 @@ #include #include "tools/cabana/commands.h" +#include "tools/cabana/utils/util.h" // SignalModel @@ -25,12 +26,12 @@ static QString signalTypeToString(cabana::Signal::Type type) { } SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(parent) { - QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &SignalModel::refresh); - QObject::connect(dbc(), &DBCManager::msgUpdated, this, &SignalModel::handleMsgChanged); - QObject::connect(dbc(), &DBCManager::msgRemoved, this, &SignalModel::handleMsgChanged); - QObject::connect(dbc(), &DBCManager::signalAdded, this, &SignalModel::handleSignalAdded); - QObject::connect(dbc(), &DBCManager::signalUpdated, this, &SignalModel::handleSignalUpdated); - QObject::connect(dbc(), &DBCManager::signalRemoved, this, &SignalModel::handleSignalRemoved); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &SignalModel::refresh); + QObject::connect(dbcNotifier(), &QtDBCNotifier::msgUpdated, this, &SignalModel::handleMsgChanged); + QObject::connect(dbcNotifier(), &QtDBCNotifier::msgRemoved, this, &SignalModel::handleMsgChanged); + QObject::connect(dbcNotifier(), &QtDBCNotifier::signalAdded, this, &SignalModel::handleSignalAdded); + QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &SignalModel::handleSignalUpdated); + QObject::connect(dbcNotifier(), &QtDBCNotifier::signalRemoved, this, &SignalModel::handleSignalRemoved); } void SignalModel::insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig) { @@ -197,7 +198,7 @@ bool SignalModel::saveSignal(const cabana::Signal *origin_s, cabana::Signal &s) if (s.is_little_endian != origin_s->is_little_endian) { s.start_bit = flipBitPos(s.start_bit); } - UndoStack::push(new EditSignalCommand(msg_id, origin_s, s)); + UndoStack::instance()->push(new EditSignalCommand(msg_id, origin_s, s)); return true; } @@ -251,7 +252,7 @@ void SignalModel::handleSignalRemoved(const cabana::Signal *sig) { SignalItemDelegate::SignalItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { name_validator = new NameValidator(this); - node_validator = new QRegExpValidator(QRegExp("^\\w+(,\\w+)*$"), this); + node_validator = new NodeValidator(this); double_validator = new DoubleValidator(this); label_font.setPointSize(8); @@ -304,7 +305,7 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op path.addRoundedRect(icon_rect, 3, 3); painter->setPen(item->highlight ? Qt::white : Qt::black); painter->setFont(label_font); - painter->fillPath(path, item->sig->color.darker(item->highlight ? 125 : 0)); + painter->fillPath(path, toQColor(item->sig->color.darker(item->highlight ? 125 : 0))); painter->drawText(icon_rect, Qt::AlignCenter, QString::number(item->row() + 1)); rect.setLeft(icon_rect.right() + h_margin * 2); @@ -375,15 +376,6 @@ QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionVie else if (item->type == SignalModel::Item::Node) e->setValidator(node_validator); else e->setValidator(double_validator); - if (item->type == SignalModel::Item::Name) { - auto names = dbc()->signalNames(); - QStringList qnames; - for (const auto &n : names) qnames.push_back(QString::fromStdString(n)); - QCompleter *completer = new QCompleter(qnames, e); - completer->setCaseSensitivity(Qt::CaseInsensitive); - completer->setFilterMode(Qt::MatchContains); - e->setCompleter(completer); - } return e; } else if (item->type == SignalModel::Item::Size) { QSpinBox *spin = new QSpinBox(parent); @@ -428,8 +420,7 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), QHBoxLayout *hl = new QHBoxLayout(title_bar); hl->addWidget(signal_count_lb = new QLabel()); filter_edit = new QLineEdit(this); - QRegularExpression re("\\S+"); - filter_edit->setValidator(new QRegularExpressionValidator(re, this)); + filter_edit->setValidator(new NonWhitespaceValidator(this)); filter_edit->setClearButtonEnabled(true); filter_edit->setPlaceholderText(tr("Filter Signal")); hl->addWidget(filter_edit); @@ -481,8 +472,8 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), QObject::connect(tree, &QTreeView::entered, [this](const QModelIndex &index) { emit highlight(model->getItem(index)->sig); }); QObject::connect(model, &QAbstractItemModel::modelReset, this, &SignalView::rowsChanged); QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &SignalView::rowsChanged); - QObject::connect(dbc(), &DBCManager::signalAdded, this, &SignalView::handleSignalAdded); - QObject::connect(dbc(), &DBCManager::signalUpdated, this, &SignalView::handleSignalUpdated); + QObject::connect(dbcNotifier(), &QtDBCNotifier::signalAdded, this, &SignalView::handleSignalAdded); + QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &SignalView::handleSignalUpdated); QObject::connect(tree->verticalScrollBar(), &QScrollBar::valueChanged, [this]() { updateState(); }); QObject::connect(tree->verticalScrollBar(), &QScrollBar::rangeChanged, [this]() { updateState(); }); QObject::connect(can, &AbstractStream::msgsReceived, this, &SignalView::updateState); @@ -524,7 +515,7 @@ void SignalView::rowsChanged() { tree->setIndexWidget(index, w); auto sig = model->getItem(index)->sig; - QObject::connect(remove_btn, &QToolButton::clicked, [=]() { UndoStack::push(new RemoveSigCommand(model->msg_id, sig)); }); + QObject::connect(remove_btn, &QToolButton::clicked, [=]() { UndoStack::instance()->push(new RemoveSigCommand(model->msg_id, sig)); }); QObject::connect(plot_btn, &QToolButton::clicked, [=](bool checked) { emit showChart(model->msg_id, sig, checked, QGuiApplication::keyboardModifiers() & Qt::ShiftModifier); }); diff --git a/tools/cabana/signalview.h b/openpilot/tools/cabana/signalview.h similarity index 93% rename from tools/cabana/signalview.h rename to openpilot/tools/cabana/signalview.h index 42db830df7..2e44e55cbf 100644 --- a/tools/cabana/signalview.h +++ b/openpilot/tools/cabana/signalview.h @@ -129,9 +129,12 @@ private: // update widget geometries in QTreeView::rowsInserted QTreeView::rowsInserted(parent, start, end); } - void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()) override { + void setModel(QAbstractItemModel *model) override { + QTreeView::setModel(model); // Bypass the slow call to QTreeView::dataChanged. - QAbstractItemView::dataChanged(topLeft, bottomRight, roles); + QObject::disconnect(model, &QAbstractItemModel::dataChanged, this, nullptr); + QObject::connect(model, &QAbstractItemModel::dataChanged, this, + [this](const QModelIndex &tl, const QModelIndex &br, const auto &roles) { QAbstractItemView::dataChanged(tl, br, roles); }); } void leaveEvent(QEvent *event) override { emit static_cast(parentWidget())->highlight(nullptr); diff --git a/tools/cabana/streams/abstractstream.cc b/openpilot/tools/cabana/streams/abstractstream.cc similarity index 93% rename from tools/cabana/streams/abstractstream.cc rename to openpilot/tools/cabana/streams/abstractstream.cc index 1582fcd34d..c58a98084f 100644 --- a/tools/cabana/streams/abstractstream.cc +++ b/openpilot/tools/cabana/streams/abstractstream.cc @@ -1,4 +1,5 @@ #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -18,8 +19,8 @@ AbstractStream::AbstractStream(QObject *parent) : QObject(parent) { QObject::connect(this, &AbstractStream::privateUpdateLastMsgsSignal, this, &AbstractStream::updateLastMessages, Qt::QueuedConnection); QObject::connect(this, &AbstractStream::seekedTo, this, &AbstractStream::updateLastMsgsTo); QObject::connect(this, &AbstractStream::seeking, this, [this](double sec) { current_sec_ = sec; }); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &AbstractStream::updateMasks); - QObject::connect(dbc(), &DBCManager::maskUpdated, this, &AbstractStream::updateMasks); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &AbstractStream::updateMasks); + QObject::connect(dbcNotifier(), &QtDBCNotifier::maskUpdated, this, &AbstractStream::updateMasks); } void AbstractStream::updateMasks() { @@ -233,18 +234,18 @@ std::pair AbstractStream::eventsInRange(const Messag namespace { enum Color { GREYISH_BLUE, CYAN, RED}; -QColor getColor(int c) { +CabanaColor getColor(int c) { constexpr int start_alpha = 128; - static const QColor colors[] = { - [GREYISH_BLUE] = QColor(102, 86, 169, start_alpha / 2), - [CYAN] = QColor(0, 187, 255, start_alpha), - [RED] = QColor(255, 0, 0, start_alpha), + static const CabanaColor colors[] = { + [GREYISH_BLUE] = CabanaColor(102, 86, 169, start_alpha / 2), + [CYAN] = CabanaColor(0, 187, 255, start_alpha), + [RED] = CabanaColor(255, 0, 0, start_alpha), }; return settings.theme == LIGHT_THEME ? colors[c] : colors[c].lighter(135); } -inline QColor blend(const QColor &a, const QColor &b) { - return QColor((a.red() + b.red()) / 2, (a.green() + b.green()) / 2, (a.blue() + b.blue()) / 2, (a.alpha() + b.alpha()) / 2); +inline CabanaColor blend(const CabanaColor &a, const CabanaColor &b) { + return CabanaColor((a.red() + b.red()) / 2, (a.green() + b.green()) / 2, (a.blue() + b.blue()) / 2, (a.alpha() + b.alpha()) / 2); } // Calculate the frequency from the past one minute data @@ -271,7 +272,7 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in if (dat.size() != size) { dat.assign(can_data, can_data + size); - colors.assign(size, QColor(0, 0, 0, 0)); + colors.assign(size, CabanaColor(0, 0, 0, 0)); last_changes.resize(size); bit_flip_counts.resize(size); std::for_each(last_changes.begin(), last_changes.end(), [current_sec](auto &c) { c.ts = current_sec; }); @@ -317,7 +318,7 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in last_change.delta = delta; } else { // Fade out - colors[i].setAlphaF(std::max(0.0, colors[i].alphaF() - alpha_delta)); + colors[i].setAlphaF(std::max(0.0f, colors[i].alphaF() - alpha_delta)); } } } diff --git a/tools/cabana/streams/abstractstream.h b/openpilot/tools/cabana/streams/abstractstream.h similarity index 76% rename from tools/cabana/streams/abstractstream.h rename to openpilot/tools/cabana/streams/abstractstream.h index 7d66a420dc..82cb899937 100644 --- a/tools/cabana/streams/abstractstream.h +++ b/openpilot/tools/cabana/streams/abstractstream.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -11,51 +12,12 @@ #include #include -#include -#include - -#include "cereal/messaging/messaging.h" +#include "openpilot/cereal/messaging/messaging.h" +#include "tools/cabana/core/can_data.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/utils/util.h" #include "tools/replay/util.h" -struct CanData { - void compute(const MessageId &msg_id, const uint8_t *dat, const int size, double current_sec, - double playback_speed, const std::vector &mask, double in_freq = 0); - - double ts = 0.; - uint32_t count = 0; - double freq = 0; - std::vector dat; - std::vector colors; - - struct ByteLastChange { - double ts = 0; - int delta = 0; - int same_delta_counter = 0; - bool suppressed = false; - }; - std::vector last_changes; - std::vector> bit_flip_counts; - double last_freq_update_ts = 0; -}; - -struct CanEvent { - uint8_t src; - uint32_t address; - uint64_t mono_time; - uint8_t size; - uint8_t dat[]; -}; - -struct CompareCanEvent { - constexpr bool operator()(const CanEvent *const e, uint64_t ts) const { return e->mono_time < ts; } - constexpr bool operator()(uint64_t ts, const CanEvent *const e) const { return ts < e->mono_time; } -}; - -typedef std::unordered_map> MessageEventsMap; -using CanEventIter = std::vector::const_iterator; - class AbstractStream : public QObject { Q_OBJECT @@ -67,7 +29,7 @@ public: virtual void seekTo(double ts) {} virtual std::string routeName() const = 0; virtual std::string carFingerprint() const { return ""; } - virtual QDateTime beginDateTime() const { return {}; } + virtual std::chrono::system_clock::time_point beginDateTime() const { return {}; } virtual uint64_t beginMonoTime() const { return 0; } virtual double minSeconds() const { return 0; } virtual double maxSeconds() const { return 0; } @@ -113,12 +75,12 @@ protected: const CanEvent *newEvent(uint64_t mono_time, const cereal::CanData::Reader &c); void updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size); void waitForSeekFinshed(); + virtual void updateLastMessages(); std::vector all_events_; double current_sec_ = 0; std::optional> time_range_; private: - void updateLastMessages(); void updateLastMsgsTo(double sec); void updateMasks(); diff --git a/openpilot/tools/cabana/streams/devicestream.cc b/openpilot/tools/cabana/streams/devicestream.cc new file mode 100644 index 0000000000..91e890316e --- /dev/null +++ b/openpilot/tools/cabana/streams/devicestream.cc @@ -0,0 +1,148 @@ +#include "tools/cabana/streams/devicestream.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "openpilot/cereal/services.h" + +#include +#include +#include +#include + +#include "tools/cabana/utils/util.h" + +// DeviceStream + +DeviceStream::DeviceStream(QObject *parent, QString address) : zmq_address(address), LiveStream(parent) { +} + +DeviceStream::~DeviceStream() { + stop(); + stopBridge(); +} + +void DeviceStream::stopBridge() { + if (bridge_pid <= 0) return; + + ::kill(bridge_pid, SIGTERM); + for (int i = 0; i < 30; ++i) { + int status = 0; + pid_t r = ::waitpid(bridge_pid, &status, WNOHANG); + if (r == bridge_pid || (r < 0 && errno == ECHILD)) { + bridge_pid = -1; + return; + } + usleep(100000); // 100ms, up to ~3s + } + ::kill(bridge_pid, SIGKILL); + ::waitpid(bridge_pid, nullptr, 0); + bridge_pid = -1; +} + +void DeviceStream::start() { + if (!zmq_address.isEmpty()) { + stopBridge(); + const std::string path = (std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()) / + "../../openpilot/cereal/messaging/bridge").lexically_normal().string(); + const std::string addr = zmq_address.toStdString(); + const char *can_filter = "/\"can/\""; + + // Self-pipe: write end is CLOEXEC so it closes on successful exec. If exec + // fails, the child writes errno and the parent aborts stream start. + int err_pipe[2] = {-1, -1}; + if (::pipe(err_pipe) != 0) { + QMessageBox::warning(nullptr, tr("Error"), + tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); + return; + } + + pid_t pid = ::fork(); + if (pid == 0) { + ::close(err_pipe[0]); + ::fcntl(err_pipe[1], F_SETFD, FD_CLOEXEC); + execl(path.c_str(), path.c_str(), addr.c_str(), can_filter, static_cast(nullptr)); + const int err = errno; + (void)!::write(err_pipe[1], &err, sizeof(err)); + _exit(127); + } + + ::close(err_pipe[1]); + if (pid < 0) { + ::close(err_pipe[0]); + QMessageBox::warning(nullptr, tr("Error"), + tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); + return; + } + + int exec_errno = 0; + const ssize_t n = ::read(err_pipe[0], &exec_errno, sizeof(exec_errno)); + ::close(err_pipe[0]); + if (n == static_cast(sizeof(exec_errno))) { + // Child failed to exec; reap and surface the error. + int status = 0; + ::waitpid(pid, &status, 0); + QMessageBox::warning(nullptr, tr("Error"), + tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(exec_errno)))); + return; + } + + bridge_pid = pid; + } + + LiveStream::start(); +} + +void DeviceStream::streamThread() { + zmq_address.isEmpty() ? unsetenv("ZMQ") : setenv("ZMQ", "1", 1); + + std::unique_ptr context(Context::create()); + std::unique_ptr sock(SubSocket::create(context.get(), "can", "127.0.0.1", false, true, services.at("can").queue_size)); + assert(sock != NULL); + // run as fast as messages come in + while (!exit_) { + std::unique_ptr msg(sock->receive(true)); + if (!msg) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + continue; + } + handleEvent(kj::ArrayPtr((capnp::word*)msg->getData(), msg->getSize() / sizeof(capnp::word))); + } +} + +// OpenDeviceWidget + +OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { + QRadioButton *msgq = new QRadioButton(tr("MSGQ")); + QRadioButton *zmq = new QRadioButton(tr("ZMQ")); + ip_address = new QLineEdit(this); + ip_address->setPlaceholderText(tr("Enter device Ip Address")); + ip_address->setValidator(new IpAddressValidator(this)); + + group = new QButtonGroup(this); + group->addButton(msgq, 0); + group->addButton(zmq, 1); + + QFormLayout *form_layout = new QFormLayout(this); + form_layout->addRow(msgq); + form_layout->addRow(zmq, ip_address); + QObject::connect(group, qOverload(&QButtonGroup::buttonToggled), [=](QAbstractButton *button, bool checked) { + ip_address->setEnabled(button == zmq && checked); + }); + zmq->setChecked(true); +} + +AbstractStream *OpenDeviceWidget::open() { + QString ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text(); + bool msgq = group->checkedId() == 0; + return new DeviceStream(qApp, msgq ? "" : ip); +} diff --git a/tools/cabana/streams/devicestream.h b/openpilot/tools/cabana/streams/devicestream.h similarity index 90% rename from tools/cabana/streams/devicestream.h rename to openpilot/tools/cabana/streams/devicestream.h index 4bcdb5351d..0e6951c92c 100644 --- a/tools/cabana/streams/devicestream.h +++ b/openpilot/tools/cabana/streams/devicestream.h @@ -2,7 +2,7 @@ #include "tools/cabana/streams/livestream.h" -#include +#include class DeviceStream : public LiveStream { Q_OBJECT @@ -16,7 +16,8 @@ public: protected: void start() override; void streamThread() override; - QProcess *bridge_process = nullptr; + void stopBridge(); + pid_t bridge_pid = -1; const QString zmq_address; }; diff --git a/tools/cabana/streams/livestream.cc b/openpilot/tools/cabana/streams/livestream.cc similarity index 64% rename from tools/cabana/streams/livestream.cc rename to openpilot/tools/cabana/streams/livestream.cc index ac9a6fa105..019a67e8f2 100644 --- a/tools/cabana/streams/livestream.cc +++ b/openpilot/tools/cabana/streams/livestream.cc @@ -1,9 +1,11 @@ #include "tools/cabana/streams/livestream.h" -#include #include +#include #include +#include #include +#include #include "common/timing.h" #include "common/util.h" @@ -14,9 +16,14 @@ struct LiveStream::Logger { void write(kj::ArrayPtr data) { int n = (seconds_since_epoch() - start_ts) / 60.0; if (std::exchange(segment_num, n) != segment_num) { + const time_t start_time = start_ts; + std::tm local_time = {}; + localtime_r(&start_time, &local_time); + std::ostringstream date; + date << std::put_time(&local_time, "%Y-%m-%d--%H-%M-%S"); QString dir = QString("%1/%2--%3") - .arg(settings.log_path) - .arg(QDateTime::fromSecsSinceEpoch(start_ts).toString("yyyy-MM-dd--hh-mm-ss")) + .arg(QString::fromStdString(settings.log_path)) + .arg(QString::fromStdString(date.str())) .arg(n); util::create_directories(dir.toStdString(), 0755); fs.reset(new std::ofstream((dir + "/rlog").toStdString(), std::ios::binary | std::ios::out)); @@ -35,37 +42,34 @@ LiveStream::LiveStream(QObject *parent) : AbstractStream(parent) { if (settings.log_livestream) { logger = std::make_unique(); } - stream_thread = new QThread(this); - - QObject::connect(&settings, &Settings::changed, this, &LiveStream::startUpdateTimer); - QObject::connect(stream_thread, &QThread::started, [=]() { streamThread(); }); - QObject::connect(stream_thread, &QThread::finished, stream_thread, &QThread::deleteLater); } LiveStream::~LiveStream() { stop(); } -void LiveStream::startUpdateTimer() { - update_timer.stop(); - update_timer.start(1000.0 / settings.fps, this); - timer_id = update_timer.timerId(); -} - void LiveStream::start() { - stream_thread->start(); - startUpdateTimer(); - begin_date_time = QDateTime::currentDateTime(); + begin_date_time = std::chrono::system_clock::now(); + fps_ = settings.fps; + exit_ = false; + stream_thread = std::thread(&LiveStream::streamThread, this); + update_thread = std::thread(&LiveStream::updateThread, this); } void LiveStream::stop() { - if (!stream_thread) return; + exit_ = true; + if (stream_thread.joinable()) stream_thread.join(); + if (update_thread.joinable()) update_thread.join(); +} - update_timer.stop(); - stream_thread->requestInterruption(); - stream_thread->quit(); - stream_thread->wait(); - stream_thread = nullptr; +void LiveStream::updateThread() { + while (!exit_) { + std::this_thread::sleep_for(std::chrono::milliseconds(1000 / fps_)); + // coalesce: skip the emit if the main thread hasn't processed the previous one yet. + if (!update_pending_.exchange(true)) { + emit privateUpdateLastMsgsSignal(); + } + } } // called in streamThread @@ -85,23 +89,22 @@ void LiveStream::handleEvent(kj::ArrayPtr data) { } } -void LiveStream::timerEvent(QTimerEvent *event) { - if (event->timerId() == timer_id) { - { - // merge events received from live stream thread. - std::lock_guard lk(lock); - mergeEvents(received_events_); - uint64_t last_received_ts = !received_events_.empty() ? received_events_.back()->mono_time : 0; - lastest_event_ts = std::max(lastest_event_ts, last_received_ts); - received_events_.clear(); - } - if (!all_events_.empty()) { - begin_event_ts = all_events_.front()->mono_time; - updateEvents(); - return; - } +// called on the main thread by the queued privateUpdateLastMsgsSignal connection +void LiveStream::updateLastMessages() { + update_pending_ = false; + fps_ = settings.fps; + { + // merge events received from live stream thread. + std::lock_guard lk(lock); + mergeEvents(received_events_); + uint64_t last_received_ts = !received_events_.empty() ? received_events_.back()->mono_time : 0; + lastest_event_ts = std::max(lastest_event_ts, last_received_ts); + received_events_.clear(); + } + if (!all_events_.empty()) { + begin_event_ts = all_events_.front()->mono_time; + updateEvents(); } - QObject::timerEvent(event); } void LiveStream::updateEvents() { @@ -131,7 +134,7 @@ void LiveStream::updateEvents() { updateEvent(id, (e->mono_time - begin_event_ts) / 1e9, e->dat, e->size); current_event_ts = e->mono_time; } - emit privateUpdateLastMsgsSignal(); + AbstractStream::updateLastMessages(); } void LiveStream::seekTo(double sec) { diff --git a/tools/cabana/streams/livestream.h b/openpilot/tools/cabana/streams/livestream.h similarity index 74% rename from tools/cabana/streams/livestream.h rename to openpilot/tools/cabana/streams/livestream.h index 24b9285092..5d65b1743f 100644 --- a/tools/cabana/streams/livestream.h +++ b/openpilot/tools/cabana/streams/livestream.h @@ -1,11 +1,11 @@ #pragma once #include +#include #include +#include #include -#include - #include "tools/cabana/streams/abstractstream.h" class LiveStream : public AbstractStream { @@ -16,7 +16,7 @@ public: virtual ~LiveStream(); void start() override; void stop(); - inline QDateTime beginDateTime() const { return begin_date_time; } + inline std::chrono::system_clock::time_point beginDateTime() const override { return begin_date_time; } inline uint64_t beginMonoTime() const override { return begin_event_ts; } double maxSeconds() const override { return std::max(1.0, (lastest_event_ts - begin_event_ts) / 1e9); } void setSpeed(float speed) override { speed_ = speed; } @@ -29,19 +29,20 @@ protected: virtual void streamThread() = 0; void handleEvent(kj::ArrayPtr event); + std::atomic exit_ = false; + private: - void startUpdateTimer(); - void timerEvent(QTimerEvent *event) override; + void updateThread(); + void updateLastMessages() override; void updateEvents(); std::mutex lock; - QThread *stream_thread; + std::thread stream_thread, update_thread; + std::atomic update_pending_ = false; + std::atomic fps_ = 10; std::vector received_events_; - int timer_id; - QBasicTimer update_timer; - - QDateTime begin_date_time; + std::chrono::system_clock::time_point begin_date_time; uint64_t begin_event_ts = 0; uint64_t lastest_event_ts = 0; uint64_t current_event_ts = 0; diff --git a/tools/cabana/streams/pandastream.cc b/openpilot/tools/cabana/streams/pandastream.cc similarity index 91% rename from tools/cabana/streams/pandastream.cc rename to openpilot/tools/cabana/streams/pandastream.cc index 3692f71a11..7ccb18a756 100644 --- a/tools/cabana/streams/pandastream.cc +++ b/openpilot/tools/cabana/streams/pandastream.cc @@ -1,11 +1,13 @@ #include "tools/cabana/streams/pandastream.h" -#include +#include +#include +#include + #include #include #include #include -#include #include PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) { @@ -16,10 +18,10 @@ PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(co bool PandaStream::connect() { try { - qDebug() << "Connecting to panda " << config.serial.c_str(); + fprintf(stderr, "Connecting to panda %s\n", config.serial.c_str()); panda.reset(new Panda(config.serial)); config.bus_config.resize(3); - qDebug() << "Connected"; + fprintf(stderr, "Connected\n"); } catch (const std::exception& e) { return false; } @@ -44,20 +46,20 @@ bool PandaStream::connect() { void PandaStream::streamThread() { std::vector raw_can_data; - while (!QThread::currentThread()->isInterruptionRequested()) { - QThread::msleep(1); + while (!exit_) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); if (!panda->connected()) { - qDebug() << "Connection to panda lost. Attempting reconnect."; + fprintf(stderr, "Connection to panda lost. Attempting reconnect.\n"); if (!connect()){ - QThread::msleep(1000); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); continue; } } raw_can_data.clear(); if (!panda->can_receive(raw_can_data)) { - qDebug() << "failed to receive"; + fprintf(stderr, "failed to receive\n"); continue; } @@ -123,7 +125,7 @@ void OpenPandaWidget::buildConfigForm() { Panda panda(serial.toStdString()); has_fd = (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA) || (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA_V2); } catch (const std::exception& e) { - qDebug() << "failed to open panda" << serial; + fprintf(stderr, "failed to open panda %s\n", serial.toUtf8().constData()); has_panda = false; } } diff --git a/tools/cabana/streams/pandastream.h b/openpilot/tools/cabana/streams/pandastream.h similarity index 100% rename from tools/cabana/streams/pandastream.h rename to openpilot/tools/cabana/streams/pandastream.h diff --git a/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc similarity index 96% rename from tools/cabana/streams/replaystream.cc rename to openpilot/tools/cabana/streams/replaystream.cc index f42bf2601c..dbd44b54e3 100644 --- a/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -1,5 +1,7 @@ #include "tools/cabana/streams/replaystream.h" +#include + #include #include #include @@ -14,10 +16,7 @@ ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) { unsetenv("ZMQ"); setenv("COMMA_CACHE", "/tmp/comma_download_cache", 1); - // TODO: Remove when OpenpilotPrefix supports ZMQ -#ifndef __APPLE__ op_prefix = std::make_unique(); -#endif QObject::connect(&settings, &Settings::changed, this, [this]() { if (replay) replay->setSegmentCacheLimit(settings.max_cached_minutes); @@ -68,7 +67,7 @@ bool ReplayStream::loadRoute(const std::string &route, const std::string &data_d QString message; if (auth_content.empty()) { message = "Authentication Required. Please run the following command to authenticate:\n\n" - "python3 tools/lib/auth.py\n\n" + "python3 openpilot/tools/lib/auth.py\n\n" "This will grant access to routes from your comma account."; } else { message = tr("Access Denied. You do not have permission to access route:\n\n%1\n\n" @@ -136,10 +135,10 @@ OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(p setMinimumWidth(550); QObject::connect(browse_local_btn, &QPushButton::clicked, [=]() { - QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), settings.last_route_dir); + QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), QString::fromStdString(settings.last_route_dir)); if (!dir.isEmpty()) { route_edit->setText(dir); - settings.last_route_dir = QFileInfo(dir).absolutePath(); + settings.last_route_dir = std::filesystem::absolute(dir.toStdString()).parent_path().string(); } }); QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() { diff --git a/tools/cabana/streams/replaystream.h b/openpilot/tools/cabana/streams/replaystream.h similarity index 92% rename from tools/cabana/streams/replaystream.h rename to openpilot/tools/cabana/streams/replaystream.h index 40f8ec8cfb..eecd345715 100644 --- a/tools/cabana/streams/replaystream.h +++ b/openpilot/tools/cabana/streams/replaystream.h @@ -26,7 +26,9 @@ public: inline std::string carFingerprint() const override { return replay->carFingerprint(); } double minSeconds() const override { return replay->minSeconds(); } double maxSeconds() const { return replay->maxSeconds(); } - inline QDateTime beginDateTime() const { return QDateTime::fromSecsSinceEpoch(replay->routeDateTime()); } + inline std::chrono::system_clock::time_point beginDateTime() const override { + return std::chrono::system_clock::from_time_t(replay->routeDateTime()); + } inline uint64_t beginMonoTime() const override { return replay->routeStartNanos(); } inline void setSpeed(float speed) override { replay->setSpeed(speed); } inline float getSpeed() const { return replay->getSpeed(); } diff --git a/tools/cabana/streams/routes.cc b/openpilot/tools/cabana/streams/routes.cc similarity index 51% rename from tools/cabana/streams/routes.cc rename to openpilot/tools/cabana/streams/routes.cc index 1e69a45cea..b6f98da533 100644 --- a/tools/cabana/streams/routes.cc +++ b/openpilot/tools/cabana/streams/routes.cc @@ -1,18 +1,19 @@ #include "tools/cabana/streams/routes.h" +#include +#include +#include +#include +#include + #include -#include #include #include -#include -#include -#include #include #include #include -#include -#include +#include "json11/json11.hpp" #include "tools/replay/py_downloader.h" namespace { @@ -20,13 +21,52 @@ namespace { // Parse a PyDownloader JSON response into (success, error_code). std::pair checkApiResponse(const std::string &result) { if (result.empty()) return {false, 500}; - auto doc = QJsonDocument::fromJson(QByteArray::fromStdString(result)); - if (doc.isObject() && doc.object().contains("error")) { - return {false, doc.object()["error"].toString() == "unauthorized" ? 401 : 500}; + std::string err; + auto doc = json11::Json::parse(result, err); + if (!err.empty()) return {false, 500}; + if (doc.is_object() && doc["error"].is_string()) { + return {false, doc["error"].string_value() == "unauthorized" ? 401 : 500}; } return {true, 0}; } +int64_t nowUnixMs() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); +} + +// Parse ISO-8601 (with optional fractional seconds / Z) to unix ms. Returns 0 on failure. +int64_t parseIsoToUnixMs(const std::string &s) { + std::string bytes = s; + if (!bytes.empty() && (bytes.back() == 'Z' || bytes.back() == 'z')) bytes.pop_back(); + int millis = 0; + auto dot = bytes.find('.'); + if (dot != std::string::npos) { + std::string frac = bytes.substr(dot + 1); + bytes = bytes.substr(0, dot); + while (frac.size() < 3) frac.push_back('0'); + millis = std::atoi(frac.substr(0, 3).c_str()); + } + std::tm tm{}; + const char *ret = strptime(bytes.c_str(), "%Y-%m-%dT%H:%M:%S", &tm); + if (!ret) ret = strptime(bytes.c_str(), "%Y-%m-%d %H:%M:%S", &tm); + if (!ret) return 0; + tm.tm_isdst = -1; + time_t secs = timegm(&tm); + if (secs == static_cast(-1)) return 0; + return static_cast(secs) * 1000 + millis; +} + +QString formatUnixMs(int64_t ms) { + time_t secs = static_cast(ms / 1000); + std::tm tm{}; + localtime_r(&secs, &tm); + char buf[64]; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); + return QString::fromUtf8(buf); +} + } // namespace // The RouteListWidget class extends QListWidget to display a custom message when empty @@ -71,11 +111,10 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) { connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); // Fetch devices - QPointer self = this; - std::thread([self]() { + std::thread([this, alive = std::weak_ptr(alive_)]() { std::string result = PyDownloader::getDevices(); - QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), response = checkApiResponse(result)]() { - if (self) self->parseDeviceList(r, response.first, response.second); + QMetaObject::invokeMethod(qApp, [this, alive, r = QString::fromStdString(result), response = checkApiResponse(result)]() { + if (!alive.expired()) parseDeviceList(r, response.first, response.second); }, Qt::QueuedConnection); }).detach(); } @@ -83,12 +122,16 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) { void RoutesDialog::parseDeviceList(const QString &json, bool success, int error_code) { if (success) { device_list_->clear(); - for (const QJsonValue &device : QJsonDocument::fromJson(json.toUtf8()).array()) { - QString dongle_id = device["dongle_id"].toString(); - device_list_->addItem(dongle_id, dongle_id); + std::string err; + auto doc = json11::Json::parse(json.toStdString(), err); + if (err.empty() && doc.is_array()) { + for (const auto &device : doc.array_items()) { + QString dongle_id = QString::fromStdString(device["dongle_id"].string_value()); + device_list_->addItem(dongle_id, dongle_id); + } } } else { - QMessageBox::warning(this, tr("Error"), error_code == 401 ? tr("Unauthorized. Authenticate with tools/lib/auth.py") : tr("Network error")); + QMessageBox::warning(this, tr("Error"), error_code == 401 ? tr("Unauthorized. Authenticate with openpilot/tools/lib/auth.py") : tr("Network error")); reject(); } } @@ -106,36 +149,38 @@ void RoutesDialog::fetchRoutes() { bool preserved = (period == -1); int64_t start_ms = 0, end_ms = 0; if (!preserved) { - QDateTime now = QDateTime::currentDateTime(); - start_ms = now.addDays(-period).toMSecsSinceEpoch(); - end_ms = now.toMSecsSinceEpoch(); + end_ms = nowUnixMs(); + start_ms = end_ms - static_cast(period) * 24LL * 60LL * 60LL * 1000LL; } int request_id = ++fetch_id_; - QPointer self = this; - std::thread([self, did, start_ms, end_ms, preserved, request_id]() { + std::thread([this, alive = std::weak_ptr(alive_), did, start_ms, end_ms, preserved, request_id]() { std::string result = PyDownloader::getDeviceRoutes(did, start_ms, end_ms, preserved); - if (!self || self->fetch_id_ != request_id) return; - QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() { - if (self && self->fetch_id_ == request_id) self->parseRouteList(r, response.first, response.second); + QMetaObject::invokeMethod(qApp, [this, alive, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() { + if (!alive.expired() && fetch_id_ == request_id) parseRouteList(r, response.first, response.second); }, Qt::QueuedConnection); }).detach(); } void RoutesDialog::parseRouteList(const QString &json, bool success, int error_code) { if (success) { - for (const QJsonValue &route : QJsonDocument::fromJson(json.toUtf8()).array()) { - QDateTime from, to; - if (period_selector_->currentData().toInt() == -1) { - from = QDateTime::fromString(route["start_time"].toString(), Qt::ISODateWithMs); - to = QDateTime::fromString(route["end_time"].toString(), Qt::ISODateWithMs); - } else { - from = QDateTime::fromMSecsSinceEpoch(route["start_time_utc_millis"].toDouble()); - to = QDateTime::fromMSecsSinceEpoch(route["end_time_utc_millis"].toDouble()); + std::string err; + auto doc = json11::Json::parse(json.toStdString(), err); + if (err.empty() && doc.is_array()) { + for (const auto &route : doc.array_items()) { + int64_t from_ms = 0, to_ms = 0; + if (period_selector_->currentData().toInt() == -1) { + from_ms = parseIsoToUnixMs(route["start_time"].string_value()); + to_ms = parseIsoToUnixMs(route["end_time"].string_value()); + } else { + from_ms = static_cast(route["start_time_utc_millis"].number_value()); + to_ms = static_cast(route["end_time_utc_millis"].number_value()); + } + const int mins = static_cast((to_ms - from_ms) / 60000); + auto item = new QListWidgetItem(QString("%1 %2min").arg(formatUnixMs(from_ms)).arg(mins)); + item->setData(Qt::UserRole, QString::fromStdString(route["fullname"].string_value())); + route_list_->addItem(item); } - auto item = new QListWidgetItem(QString("%1 %2min").arg(from.toString()).arg(from.secsTo(to) / 60)); - item->setData(Qt::UserRole, route["fullname"].toString()); - route_list_->addItem(item); } if (route_list_->count() > 0) route_list_->setCurrentRow(0); } else { diff --git a/tools/cabana/streams/routes.h b/openpilot/tools/cabana/streams/routes.h similarity index 75% rename from tools/cabana/streams/routes.h rename to openpilot/tools/cabana/streams/routes.h index 99fa67ef8c..6ed145603f 100644 --- a/tools/cabana/streams/routes.h +++ b/openpilot/tools/cabana/streams/routes.h @@ -1,6 +1,8 @@ #pragma once #include +#include + #include #include @@ -21,4 +23,6 @@ protected: QComboBox *period_selector_; RouteListWidget *route_list_; std::atomic fetch_id_{0}; + // expires on destruction; guards main-thread callbacks from detached worker threads + std::shared_ptr alive_ = std::make_shared(true); }; diff --git a/tools/cabana/streams/socketcanstream.cc b/openpilot/tools/cabana/streams/socketcanstream.cc similarity index 83% rename from tools/cabana/streams/socketcanstream.cc rename to openpilot/tools/cabana/streams/socketcanstream.cc index 768465d5a3..b616e7f242 100644 --- a/tools/cabana/streams/socketcanstream.cc +++ b/openpilot/tools/cabana/streams/socketcanstream.cc @@ -7,20 +7,21 @@ #include #include -#include -#include +#include +#include +#include + #include #include #include #include -#include SocketCanStream::SocketCanStream(QObject *parent, SocketCanStreamConfig config_) : config(config_), LiveStream(parent) { if (!available()) { throw std::runtime_error("SocketCAN not available"); } - qDebug() << "Connecting to SocketCAN device" << config.device.c_str(); + fprintf(stderr, "Connecting to SocketCAN device %s\n", config.device.c_str()); if (!connect()) { throw std::runtime_error("Failed to connect to SocketCAN device"); } @@ -44,7 +45,7 @@ bool SocketCanStream::available() { bool SocketCanStream::connect() { sock_fd = socket(PF_CAN, SOCK_RAW, CAN_RAW); if (sock_fd < 0) { - qDebug() << "Failed to create CAN socket"; + fprintf(stderr, "Failed to create CAN socket\n"); return false; } @@ -55,7 +56,7 @@ bool SocketCanStream::connect() { struct ifreq ifr = {}; strncpy(ifr.ifr_name, config.device.c_str(), IFNAMSIZ - 1); if (ioctl(sock_fd, SIOCGIFINDEX, &ifr) < 0) { - qDebug() << "Failed to get interface index for" << config.device.c_str(); + fprintf(stderr, "Failed to get interface index for %s\n", config.device.c_str()); ::close(sock_fd); sock_fd = -1; return false; @@ -65,7 +66,7 @@ bool SocketCanStream::connect() { addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; if (bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { - qDebug() << "Failed to bind CAN socket"; + fprintf(stderr, "Failed to bind CAN socket\n"); ::close(sock_fd); sock_fd = -1; return false; @@ -81,7 +82,7 @@ bool SocketCanStream::connect() { void SocketCanStream::streamThread() { struct canfd_frame frame; - while (!QThread::currentThread()->isInterruptionRequested()) { + while (!exit_) { ssize_t nbytes = read(sock_fd, &frame, sizeof(frame)); if (nbytes <= 0) continue; @@ -127,14 +128,12 @@ OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWi void OpenSocketCanWidget::refreshDevices() { device_edit->clear(); // Scan /sys/class/net/ for CAN interfaces (type 280 = ARPHRD_CAN) - QDir net_dir("/sys/class/net"); - for (const auto &iface : net_dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { - QFile type_file(net_dir.filePath(iface) + "/type"); - if (type_file.open(QIODevice::ReadOnly)) { - int type = type_file.readAll().trimmed().toInt(); - if (type == 280) { - device_edit->addItem(iface); - } + std::error_code ec; + for (const auto &entry : std::filesystem::directory_iterator("/sys/class/net", ec)) { + std::ifstream type_file(entry.path() / "type"); + int type = 0; + if (type_file >> type && type == 280) { + device_edit->addItem(QString::fromStdString(entry.path().filename().string())); } } } diff --git a/tools/cabana/streams/socketcanstream.h b/openpilot/tools/cabana/streams/socketcanstream.h similarity index 100% rename from tools/cabana/streams/socketcanstream.h rename to openpilot/tools/cabana/streams/socketcanstream.h diff --git a/tools/cabana/streamselector.cc b/openpilot/tools/cabana/streamselector.cc similarity index 92% rename from tools/cabana/streamselector.cc rename to openpilot/tools/cabana/streamselector.cc index 4ad552d4b4..7e8adc568d 100644 --- a/tools/cabana/streamselector.cc +++ b/openpilot/tools/cabana/streamselector.cc @@ -1,5 +1,7 @@ #include "tools/cabana/streamselector.h" +#include + #include #include #include @@ -52,10 +54,10 @@ StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) { setEnabled(true); }); QObject::connect(file_btn, &QPushButton::clicked, [this]() { - QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)"); + QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), QString::fromStdString(settings.last_dir), "DBC (*.dbc)"); if (!fn.isEmpty()) { dbc_file->setText(fn); - settings.last_dir = QFileInfo(fn).absolutePath(); + settings.last_dir = std::filesystem::absolute(fn.toStdString()).parent_path().string(); } }); } diff --git a/tools/cabana/streamselector.h b/openpilot/tools/cabana/streamselector.h similarity index 100% rename from tools/cabana/streamselector.h rename to openpilot/tools/cabana/streamselector.h diff --git a/tools/cabana/tests/test_cabana.cc b/openpilot/tools/cabana/tests/test_cabana.cc similarity index 63% rename from tools/cabana/tests/test_cabana.cc rename to openpilot/tools/cabana/tests/test_cabana.cc index 833cfbe4b5..c66f57d593 100644 --- a/tools/cabana/tests/test_cabana.cc +++ b/openpilot/tools/cabana/tests/test_cabana.cc @@ -1,9 +1,16 @@ #undef INFO -#include +#include +#include #include "catch2/catch.hpp" +#include "tools/cabana/dbc/dbcfile.h" #include "tools/cabana/dbc/dbcmanager.h" +#include "tools/cabana/core/settings.h" + +#ifdef QT_CORE_LIB +#include +#endif const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; @@ -143,15 +150,76 @@ CM_ SG_ 162 signal_1 "signal comment with \"escaped quotes\""; } TEST_CASE("parse_opendbc") { - QDir dir(OPENDBC_FILE_PATH); - QStringList errors; - for (auto fn : dir.entryList({"*.dbc"}, QDir::Files, QDir::Name)) { + std::vector errors; + for (const auto &entry : std::filesystem::directory_iterator(OPENDBC_FILE_PATH)) { + if (!entry.is_regular_file() || entry.path().extension() != ".dbc") continue; try { - auto dbc = DBCFile(dir.filePath(fn).toStdString()); + auto dbc = DBCFile(entry.path().string()); } catch (std::exception &e) { errors.push_back(e.what()); } } - INFO(errors.join("\n").toStdString()); + std::ostringstream details; + for (const auto &error : errors) details << error << '\n'; + INFO(details.str()); REQUIRE(errors.empty()); } + +TEST_CASE("DBCManager core callbacks") { + DBCManager manager; + int files_changed = 0; + int signals_added = 0; + int masks_updated = 0; + manager.setCallbacks({ + .signal_added = [&](MessageId, const cabana::Signal *) { ++signals_added; }, + .file_changed = [&]() { ++files_changed; }, + .mask_updated = [&]() { ++masks_updated; }, + }); + + std::string error; + REQUIRE(manager.open(SOURCE_ALL, "test", "BO_ 160 message: 8 XXX\n", &error)); + REQUIRE(error.empty()); + REQUIRE(files_changed == 1); + + cabana::Signal signal{}; + signal.name = "speed"; + signal.start_bit = 0; + signal.size = 8; + signal.is_little_endian = true; + manager.addSignal({.source = 0, .address = 160}, signal); + REQUIRE(signals_added == 1); + REQUIRE(masks_updated == 1); + REQUIRE(manager.msg({.source = 0, .address = 160})->sig("speed") != nullptr); +} + +TEST_CASE("Cabana settings core defaults") { + CabanaSettingsState state; + REQUIRE(state.fps == 10); + REQUIRE(state.chart_range == 180); + REQUIRE(state.drag_direction == CabanaSettingsState::MsbFirst); + REQUIRE(state.recent_files.empty()); +} + +#ifdef QT_CORE_LIB +TEST_CASE("CabanaColor preserves QColor transformations") { + const std::vector colors = { + QColor(102, 86, 169, 64), QColor(0, 187, 255, 128), QColor(255, 0, 0, 128), QColor(45, 120, 75, 255), + }; + for (const auto &qt_color : colors) { + CabanaColor color(qt_color.red(), qt_color.green(), qt_color.blue(), qt_color.alpha()); + for (int factor : {75, 100, 135, 150, 200}) { + const auto lighter = color.lighter(factor); + const auto qt_lighter = qt_color.lighter(factor); + CHECK(std::abs(lighter.red() - qt_lighter.red()) <= 1); + CHECK(std::abs(lighter.green() - qt_lighter.green()) <= 1); + CHECK(std::abs(lighter.blue() - qt_lighter.blue()) <= 1); + + const auto darker = color.darker(factor); + const auto qt_darker = qt_color.darker(factor); + CHECK(std::abs(darker.red() - qt_darker.red()) <= 1); + CHECK(std::abs(darker.green() - qt_darker.green()) <= 1); + CHECK(std::abs(darker.blue() - qt_darker.blue()) <= 1); + } + } +} +#endif diff --git a/tools/cabana/tests/test_runner.cc b/openpilot/tools/cabana/tests/test_runner.cc similarity index 67% rename from tools/cabana/tests/test_runner.cc rename to openpilot/tools/cabana/tests/test_runner.cc index b20ac86c64..a76c8e16b9 100644 --- a/tools/cabana/tests/test_runner.cc +++ b/openpilot/tools/cabana/tests/test_runner.cc @@ -1,10 +1,7 @@ #define CATCH_CONFIG_RUNNER #include "catch2/catch.hpp" -#include int main(int argc, char **argv) { - // unit tests for Qt - QCoreApplication app(argc, argv); const int res = Catch::Session().run(argc, argv); return (res < 0xff ? res : 0xff); } diff --git a/tools/cabana/tools/findsignal.cc b/openpilot/tools/cabana/tools/findsignal.cc similarity index 96% rename from tools/cabana/tools/findsignal.cc rename to openpilot/tools/cabana/tools/findsignal.cc index b131893942..4511af7c01 100644 --- a/tools/cabana/tools/findsignal.cc +++ b/openpilot/tools/cabana/tools/findsignal.cc @@ -1,5 +1,6 @@ #include "tools/cabana/tools/findsignal.h" +#include #include #include @@ -98,9 +99,9 @@ FindSignalDlg::FindSignalDlg(QWidget *parent) : QDialog(parent, Qt::WindowFlags( message_group = new QGroupBox(tr("Messages"), this); QFormLayout *message_layout = new QFormLayout(message_group); message_layout->addRow(tr("Bus"), bus_edit = new QLineEdit()); - bus_edit->setPlaceholderText(tr("comma-seperated values. Leave blank for all")); + bus_edit->setPlaceholderText(tr("comma-separated values. Leave blank for all")); message_layout->addRow(tr("Address"), address_edit = new QLineEdit()); - address_edit->setPlaceholderText(tr("comma-seperated hex values. Leave blank for all")); + address_edit->setPlaceholderText(tr("comma-separated hex values. Leave blank for all")); QHBoxLayout *hlayout = new QHBoxLayout(); hlayout->addWidget(first_time_edit = new QLineEdit("0")); hlayout->addWidget(new QLabel("-")); @@ -210,13 +211,13 @@ void FindSignalDlg::search() { } void FindSignalDlg::setInitialSignals() { - QSet buses; + std::set buses; for (auto bus : bus_edit->text().trimmed().split(",")) { bus = bus.trimmed(); if (!bus.isEmpty()) buses.insert(bus.toUShort()); } - QSet addresses; + std::set addresses; for (auto addr : address_edit->text().trimmed().split(",")) { addr = addr.trimmed(); if (!addr.isEmpty()) addresses.insert(addr.toULong(nullptr, 16)); @@ -239,7 +240,7 @@ void FindSignalDlg::setInitialSignals() { model->initial_signals.clear(); for (const auto &[id, m] : can->lastMessages()) { - if ((buses.isEmpty() || buses.contains(id.source)) && (addresses.isEmpty() || addresses.contains(id.address))) { + if ((buses.empty() || buses.count(id.source)) && (addresses.empty() || addresses.count(id.address))) { const auto &events = can->events(id); auto e = std::lower_bound(events.cbegin(), events.cend(), first_time, CompareCanEvent()); if (e != events.cend()) { @@ -276,7 +277,7 @@ void FindSignalDlg::customMenuRequested(const QPoint &pos) { menu.addAction(tr("Create Signal")); if (menu.exec(view->mapToGlobal(pos))) { auto &s = model->filtered_signals[index.row()]; - UndoStack::push(new AddSigCommand(s.id, s.sig)); + UndoStack::instance()->push(new AddSigCommand(s.id, s.sig)); emit openMessage(s.id); } } diff --git a/tools/cabana/tools/findsignal.h b/openpilot/tools/cabana/tools/findsignal.h similarity index 100% rename from tools/cabana/tools/findsignal.h rename to openpilot/tools/cabana/tools/findsignal.h diff --git a/tools/cabana/tools/findsimilarbits.cc b/openpilot/tools/cabana/tools/findsimilarbits.cc similarity index 100% rename from tools/cabana/tools/findsimilarbits.cc rename to openpilot/tools/cabana/tools/findsimilarbits.cc diff --git a/tools/cabana/tools/findsimilarbits.h b/openpilot/tools/cabana/tools/findsimilarbits.h similarity index 100% rename from tools/cabana/tools/findsimilarbits.h rename to openpilot/tools/cabana/tools/findsimilarbits.h diff --git a/tools/cabana/tools/routeinfo.cc b/openpilot/tools/cabana/tools/routeinfo.cc similarity index 100% rename from tools/cabana/tools/routeinfo.cc rename to openpilot/tools/cabana/tools/routeinfo.cc diff --git a/tools/cabana/tools/routeinfo.h b/openpilot/tools/cabana/tools/routeinfo.h similarity index 100% rename from tools/cabana/tools/routeinfo.h rename to openpilot/tools/cabana/tools/routeinfo.h diff --git a/tools/cabana/utils/elidedlabel.cc b/openpilot/tools/cabana/utils/elidedlabel.cc similarity index 100% rename from tools/cabana/utils/elidedlabel.cc rename to openpilot/tools/cabana/utils/elidedlabel.cc diff --git a/tools/cabana/utils/elidedlabel.h b/openpilot/tools/cabana/utils/elidedlabel.h similarity index 100% rename from tools/cabana/utils/elidedlabel.h rename to openpilot/tools/cabana/utils/elidedlabel.h diff --git a/openpilot/tools/cabana/utils/export.cc b/openpilot/tools/cabana/utils/export.cc new file mode 100644 index 0000000000..d585827ef5 --- /dev/null +++ b/openpilot/tools/cabana/utils/export.cc @@ -0,0 +1,45 @@ +#include "tools/cabana/utils/export.h" + +#include +#include + +#include "tools/cabana/streams/abstractstream.h" + +namespace utils { + +void exportToCSV(const std::string &file_name, std::optional msg_id) { + std::ofstream stream(file_name, std::ios::trunc); + if (stream) { + stream << "time,addr,bus,data\n"; + for (auto e : msg_id ? can->events(*msg_id) : can->allEvents()) { + stream << std::fixed << std::setprecision(3) << can->toSeconds(e->mono_time) << "," + << "0x" << std::hex << e->address << std::dec << "," << static_cast(e->src) << ",0x" + << std::uppercase << std::hex << std::setfill('0'); + for (int i = 0; i < e->size; ++i) stream << std::setw(2) << static_cast(e->dat[i]); + stream << std::nouppercase << std::dec << "\n"; + } + } +} + +void exportSignalsToCSV(const std::string &file_name, const MessageId &msg_id) { + std::ofstream stream(file_name, std::ios::trunc); + if (auto msg = dbc()->msg(msg_id); msg && !msg->sigs.empty() && stream) { + stream << "time,addr,bus"; + for (auto s : msg->sigs) + stream << "," << s->name.c_str(); + stream << "\n"; + + for (auto e : can->events(msg_id)) { + stream << std::fixed << std::setprecision(3) << can->toSeconds(e->mono_time) << "," + << "0x" << std::hex << e->address << std::dec << "," << static_cast(e->src); + for (auto s : msg->sigs) { + double value = 0; + s->getValue(e->dat, e->size, &value); + stream << "," << std::fixed << std::setprecision(s->precision) << value; + } + stream << "\n"; + } + } +} + +} // namespace utils diff --git a/openpilot/tools/cabana/utils/export.h b/openpilot/tools/cabana/utils/export.h new file mode 100644 index 0000000000..ee110e8321 --- /dev/null +++ b/openpilot/tools/cabana/utils/export.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +#include "tools/cabana/dbc/dbcmanager.h" + +namespace utils { +void exportToCSV(const std::string &file_name, std::optional msg_id = std::nullopt); +void exportSignalsToCSV(const std::string &file_name, const MessageId &msg_id); +} // namespace utils diff --git a/tools/cabana/utils/util.cc b/openpilot/tools/cabana/utils/util.cc similarity index 52% rename from tools/cabana/utils/util.cc rename to openpilot/tools/cabana/utils/util.cc index 50ab764423..87a0f89427 100644 --- a/tools/cabana/utils/util.cc +++ b/openpilot/tools/cabana/utils/util.cc @@ -1,21 +1,24 @@ #include "tools/cabana/utils/util.h" #include +#include +#include +#include +#include +#include #include +#include +#include #include #include #include #include +#include #include #include -#include -#include #include -#include #include -#include -#include #include #include #include "common/util.h" @@ -100,7 +103,7 @@ void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem & // Paint hex column const auto &bytes = *static_cast *>(data.value()); - const auto &colors = *static_cast *>(index.data(ColorsRole).value()); + const auto &colors = *static_cast *>(index.data(ColorsRole).value()); painter->setFont(fixed_font); const QPen text_pen(option.state & QStyle::State_Selected ? highlighted_color : text_color); @@ -115,7 +118,7 @@ void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem & painter->setPen(option.palette.color(QPalette::Text)); painter->fillRect(r, option.palette.color(QPalette::Window)); } - painter->fillRect(r, colors[i]); + painter->fillRect(r, toQColor(colors[i])); } else { painter->setPen(text_pen); } @@ -148,18 +151,35 @@ void TabBar::closeTabClicked() { // UnixSignalHandler -UnixSignalHandler::UnixSignalHandler(QObject *parent) : QObject(nullptr) { +UnixSignalHandler::UnixSignalHandler() { if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sig_fd)) { qFatal("Couldn't create TERM socketpair"); } - sn = new QSocketNotifier(sig_fd[1], QSocketNotifier::Read, this); - connect(sn, &QSocketNotifier::activated, this, &UnixSignalHandler::handleSigTerm); + waiter = std::thread([this]() { + int tmp = 0; + while (::read(sig_fd[1], &tmp, sizeof(tmp)) < 0) { + if (errno != EINTR) return; + } + if (shutting_down.load()) return; + + // Marshal exit onto the GUI thread (qApp methods are not thread-safe). + QMetaObject::invokeMethod(qApp, []() { + printf("\nexiting...\n"); + qApp->closeAllWindows(); + qApp->exit(); + }, Qt::QueuedConnection); + }); + std::signal(SIGINT, signalHandler); std::signal(SIGTERM, UnixSignalHandler::signalHandler); } UnixSignalHandler::~UnixSignalHandler() { + shutting_down.store(true); + int dummy = 0; + (void)!::write(sig_fd[0], &dummy, sizeof(dummy)); + if (waiter.joinable()) waiter.join(); ::close(sig_fd[0]); ::close(sig_fd[1]); } @@ -168,34 +188,174 @@ void UnixSignalHandler::signalHandler(int s) { (void)!::write(sig_fd[0], &s, sizeof(s)); } -void UnixSignalHandler::handleSigTerm() { - sn->setEnabled(false); - int tmp; - (void)!::read(sig_fd[1], &tmp, sizeof(tmp)); - - printf("\nexiting...\n"); - qApp->closeAllWindows(); - qApp->exit(); -} - // NameValidator -NameValidator::NameValidator(QObject *parent) : QRegExpValidator(QRegExp("^(\\w+)"), parent) {} +NameValidator::NameValidator(QObject *parent) : QValidator(parent) {} QValidator::State NameValidator::validate(QString &input, int &pos) const { + Q_UNUSED(pos); input.replace(' ', '_'); - return QRegExpValidator::validate(input, pos); + if (input.isEmpty()) return QValidator::Intermediate; + for (const QChar &c : input) { + if (!c.isLetterOrNumber() && c != '_') return QValidator::Invalid; + } + return QValidator::Acceptable; } -DoubleValidator::DoubleValidator(QObject *parent) : QDoubleValidator(parent) { - // Match locale of QString::toDouble() instead of system - QLocale locale(QLocale::C); - locale.setNumberOptions(QLocale::RejectGroupSeparator); - setLocale(locale); +// NodeValidator + +NodeValidator::NodeValidator(QObject *parent) : QValidator(parent) {} + +QValidator::State NodeValidator::validate(QString &input, int &pos) const { + Q_UNUSED(pos); + if (input.isEmpty()) return QValidator::Intermediate; + // Match ^\w+(,\w+)*$ ; a trailing comma is Intermediate (user still typing). + bool need_word = true; + for (const QChar &c : input) { + if (c.isLetterOrNumber() || c == '_') { + need_word = false; + } else if (c == ',' && !need_word) { + need_word = true; + } else { + return QValidator::Invalid; + } + } + return need_word ? QValidator::Intermediate : QValidator::Acceptable; +} + +// NonWhitespaceValidator + +NonWhitespaceValidator::NonWhitespaceValidator(QObject *parent) : QValidator(parent) {} + +QValidator::State NonWhitespaceValidator::validate(QString &input, int &pos) const { + Q_UNUSED(pos); + if (input.isEmpty()) return QValidator::Intermediate; + for (const QChar &c : input) { + if (c.isSpace()) return QValidator::Invalid; + } + return QValidator::Acceptable; +} + +// IpAddressValidator + +IpAddressValidator::IpAddressValidator(QObject *parent) : QValidator(parent) {} + +QValidator::State IpAddressValidator::validate(QString &input, int &pos) const { + Q_UNUSED(pos); + if (input.isEmpty()) return QValidator::Intermediate; + + int dots = 0; + int value = 0; + bool has_digit = false; + for (const QChar &c : input) { + if (c.isDigit()) { + value = has_digit ? value * 10 + c.digitValue() : c.digitValue(); + if (value > 255) return QValidator::Invalid; + has_digit = true; + } else if (c == '.') { + if (!has_digit || dots >= 3) return QValidator::Invalid; + ++dots; + has_digit = false; + value = 0; + } else { + return QValidator::Invalid; + } + } + return (dots == 3 && has_digit) ? QValidator::Acceptable : QValidator::Intermediate; +} + +DoubleValidator::DoubleValidator(QObject *parent) : QValidator(parent) {} + +QValidator::State DoubleValidator::validate(QString &input, int &pos) const { + Q_UNUSED(pos); + if (input.isEmpty()) return QValidator::Intermediate; + + // Match QString::toDouble(): C locale, no hex floats / inf / nan. + const std::string bytes = input.toLatin1().toStdString(); + // strtod accepts 0x… hex floats and p-exponents; QString::toDouble does not. + if (bytes.find_first_of("xXpP") != std::string::npos) { + return QValidator::Invalid; + } + + const char *start = bytes.c_str(); + char *end = nullptr; + const double value = std::strtod(start, &end); + if (end == start) { + // Still typing a sign, decimal point, or exponent prefix. + if (input == "-" || input == "+" || input == "." || input == "-." || input == "+.") { + return QValidator::Intermediate; + } + return QValidator::Invalid; + } + if (*end == '\0') { + // Reject inf/nan (strtod accepts them; QDoubleValidator / toDouble path should not). + return std::isfinite(value) ? QValidator::Acceptable : QValidator::Invalid; + } + + // Partial exponent / trailing sign while typing (e.g. "1e", "1e-", "1."). + for (const char *p = end; *p; ++p) { + const char c = *p; + if (!(c == 'e' || c == 'E' || c == '+' || c == '-' || c == '.' || (c >= '0' && c <= '9'))) { + return QValidator::Invalid; + } + } + return QValidator::Intermediate; } namespace utils { +std::string homePath() { + const char *home = ::getenv("HOME"); + return home ? home : ""; +} + +std::filesystem::path configPath() { +#ifdef __APPLE__ + return std::filesystem::path(homePath()) / "Library/Preferences"; +#else + const char *xdg = ::getenv("XDG_CONFIG_HOME"); + return (xdg && xdg[0]) ? std::filesystem::path(xdg) : std::filesystem::path(homePath()) / ".config"; +#endif +} + +#ifdef __APPLE__ +static const char *clipboard_read_cmds[] = {"pbpaste"}; +static const char *clipboard_write_cmds[] = {"pbcopy"}; +#else +static const char *clipboard_read_cmds[] = {"wl-paste --no-newline 2>/dev/null", "xclip -selection clipboard -o 2>/dev/null", "xsel -ob 2>/dev/null"}; +static const char *clipboard_write_cmds[] = {"wl-copy 2>/dev/null", "xclip -selection clipboard 2>/dev/null", "xsel -ib 2>/dev/null"}; +#endif + +bool getClipboardText(std::string *text) { + text->clear(); + bool has_tool = false; + for (const char *cmd : clipboard_read_cmds) { + FILE *f = ::popen(cmd, "r"); + if (!f) continue; + std::string out; + char buf[4096]; + for (size_t n; (n = ::fread(buf, 1, sizeof(buf), f)) > 0;) out.append(buf, n); + int status = ::pclose(f); + if (status == 0) { + *text = std::move(out); + return true; + } + has_tool |= WIFEXITED(status) && WEXITSTATUS(status) != 127; // 127: command not found + } + return has_tool; // tool present but clipboard empty +} + +bool setClipboardText(const std::string &text) { + std::signal(SIGPIPE, SIG_IGN); + for (const char *cmd : clipboard_write_cmds) { + FILE *f = ::popen(cmd, "w"); + if (!f) continue; + size_t written = ::fwrite(text.data(), 1, text.size(), f); + if (::pclose(f) == 0 && written == text.size()) return true; + } + return false; +} + bool isDarkTheme() { QColor windowColor = QApplication::palette().color(QPalette::Window); return windowColor.lightness() < 128; @@ -257,10 +417,34 @@ void setTheme(int theme) { } QString formatSeconds(double sec, bool include_milliseconds, bool absolute_time) { - QString format = absolute_time ? "yyyy-MM-dd hh:mm:ss" - : (sec > 60 * 60 ? "hh:mm:ss" : "mm:ss"); - if (include_milliseconds) format += ".zzz"; - return QDateTime::fromMSecsSinceEpoch(sec * 1000).toString(format); + if (absolute_time) { + const auto ms_total = static_cast(std::llround(sec * 1000.0)); + const std::time_t secs = static_cast(ms_total / 1000); + int millis = static_cast(ms_total % 1000); + if (millis < 0) millis = -millis; + std::tm tm{}; + localtime_r(&secs, &tm); + char buf[64]; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); + if (include_milliseconds) { + return QString::asprintf("%s.%03d", buf, millis); + } + return QString::fromUtf8(buf); + } + + // Relative duration (not wall-clock). + const bool show_hours = sec > 60 * 60; + int total_ms = static_cast(std::llround(std::max(0.0, sec) * 1000.0)); + const int hours = total_ms / (3600 * 1000); + const int minutes = (total_ms / (60 * 1000)) % 60; + const int seconds = (total_ms / 1000) % 60; + const int millis = total_ms % 1000; + if (show_hours) { + return include_milliseconds ? QString::asprintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis) + : QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds); + } + return include_milliseconds ? QString::asprintf("%02d:%02d.%03d", minutes, seconds, millis) + : QString::asprintf("%02d:%02d", minutes, seconds); } } // namespace utils @@ -281,20 +465,6 @@ QString signalToolTip(const cabana::Signal *sig) { .arg(sig->is_little_endian ? "Y" : "N").arg(sig->is_signed ? "Y" : "N"); } -void setSurfaceFormat() { - QSurfaceFormat fmt; -#ifdef __APPLE__ - fmt.setVersion(3, 2); - fmt.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); - fmt.setRenderableType(QSurfaceFormat::OpenGL); -#else - fmt.setRenderableType(QSurfaceFormat::OpenGLES); -#endif - fmt.setSamples(16); - fmt.setStencilBufferSize(1); - QSurfaceFormat::setDefaultFormat(fmt); -} - void sigTermHandler(int s) { std::signal(s, SIG_DFL); qApp->quit(); @@ -305,57 +475,57 @@ void initApp(int argc, char *argv[], bool disable_hidpi) { std::signal(SIGINT, sigTermHandler); std::signal(SIGTERM, sigTermHandler); - QString app_dir; + std::filesystem::path app_dir; #ifdef __APPLE__ // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering QApplication tmp(argc, argv); - app_dir = QCoreApplication::applicationDirPath(); + app_dir = QCoreApplication::applicationDirPath().toStdString(); if (disable_hidpi) { qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit()); } #else - app_dir = QFileInfo(util::readlink("/proc/self/exe").c_str()).path(); + app_dir = std::filesystem::path(util::readlink("/proc/self/exe")).parent_path(); #endif - qputenv("QT_DBL_CLICK_DIST", QByteArray::number(150)); + qputenv("QT_DBL_CLICK_DIST", "150"); // ensure the current dir matches the exectuable's directory - QDir::setCurrent(app_dir); - - setSurfaceFormat(); + std::error_code ec; + std::filesystem::current_path(app_dir, ec); } +// embedded at build time from the bootstrap_icons package (see SConscript) +extern const unsigned char bootstrap_icons_svg[]; +extern const size_t bootstrap_icons_svg_len; + static std::unordered_map load_bootstrap_icons() { std::unordered_map icons; - QFile f(":/bootstrap-icons.svg"); - if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { - std::string content = f.readAll().toStdString(); - const std::string sym_open = "(bootstrap_icons_svg), bootstrap_icons_svg_len); + const std::string sym_open = " with - svg_str.replace(0, 7, " ""); // "" (9) -> "" (6) - icons[id] = std::move(svg_str); - } + // extract id + size_t id_start = content.find(id_attr, pos); + if (id_start != std::string::npos && id_start < end) { + id_start += id_attr.size(); + size_t id_end = content.find('"', id_start); + if (id_end != std::string::npos && id_end < end) { + std::string id = content.substr(id_start, id_end - id_start); + std::string svg_str = content.substr(pos, end - pos); + // replace with + svg_str.replace(0, 7, " ""); // "" (9) -> "" (6) + icons[id] = std::move(svg_str); } - pos = end; } + pos = end; } return icons; } diff --git a/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h similarity index 65% rename from tools/cabana/utils/util.h rename to openpilot/tools/cabana/utils/util.h index f839ffe7fe..5a3c62d118 100644 --- a/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -1,26 +1,32 @@ #pragma once #include +#include #include +#include +#include +#include #include #include #include -#include -#include +#include #include #include #include -#include -#include #include #include #include #include +#include #include "tools/cabana/dbc/dbc.h" #include "tools/cabana/settings.h" +inline QColor toQColor(const CabanaColor &color) { + return QColor(color.r, color.g, color.b, color.a); +} + class LogSlider : public QSlider { Q_OBJECT @@ -84,22 +90,53 @@ private: int h_margin, v_margin; }; -class NameValidator : public QRegExpValidator { +// Accepts a single identifier: one or more [A-Za-z0-9_], spaces rewritten to '_'. +class NameValidator : public QValidator { Q_OBJECT public: NameValidator(QObject *parent=nullptr); QValidator::State validate(QString &input, int &pos) const override; }; -class DoubleValidator : public QDoubleValidator { +// Accepts comma-separated identifiers: \w+(,\w+)* +class NodeValidator : public QValidator { + Q_OBJECT +public: + NodeValidator(QObject *parent=nullptr); + QValidator::State validate(QString &input, int &pos) const override; +}; + +// Accepts one or more non-whitespace characters (\S+). +class NonWhitespaceValidator : public QValidator { + Q_OBJECT +public: + NonWhitespaceValidator(QObject *parent=nullptr); + QValidator::State validate(QString &input, int &pos) const override; +}; + +// Accepts a dotted IPv4 address (0-255 per octet). +class IpAddressValidator : public QValidator { + Q_OBJECT +public: + IpAddressValidator(QObject *parent=nullptr); + QValidator::State validate(QString &input, int &pos) const override; +}; + +// C-locale floating-point validator (matches QString::toDouble). +class DoubleValidator : public QValidator { Q_OBJECT public: DoubleValidator(QObject *parent = nullptr); + QValidator::State validate(QString &input, int &pos) const override; }; namespace utils { QPixmap icon(const QString &id); +std::string homePath(); +std::filesystem::path configPath(); +bool getClipboardText(std::string *text); // false if no clipboard tool is available +bool setClipboardText(const std::string &text); bool isDarkTheme(); void setTheme(int theme); QString formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false); @@ -108,7 +145,22 @@ inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text); } inline QString toHex(const std::vector &dat, char separator = '\0') { - return QByteArray::fromRawData((const char *)dat.data(), dat.size()).toHex(separator).toUpper(); + static const char digits[] = "0123456789ABCDEF"; + QString hex; + hex.reserve(dat.size() * (separator ? 3 : 2)); + for (size_t i = 0; i < dat.size(); ++i) { + if (separator && i) hex += QLatin1Char(separator); + hex += QLatin1Char(digits[dat[i] >> 4]); + hex += QLatin1Char(digits[dat[i] & 0xf]); + } + return hex; +} + +// boundary conversions for the remaining Qt byte-array based state APIs +template +std::vector toBytes(const T &dat) { return {dat.begin(), dat.end()}; } +inline auto qbytes(const std::vector &dat) { + return decltype(QString().toUtf8())((const char *)dat.data(), (int)dat.size()); } } @@ -147,20 +199,18 @@ private: void closeTabClicked(); }; -class UnixSignalHandler : public QObject { - Q_OBJECT - +// Watches SIGINT/SIGTERM via a self-pipe and a dedicated waiter thread +// (no Qt notifiers/timers). Exit is marshaled onto the GUI thread. +class UnixSignalHandler { public: - UnixSignalHandler(QObject *parent = nullptr); + UnixSignalHandler(); ~UnixSignalHandler(); static void signalHandler(int s); -public slots: - void handleSigTerm(); - private: inline static int sig_fd[2] = {}; - QSocketNotifier *sn; + std::atomic shutting_down{false}; + std::thread waiter; }; int num_decimals(double num); diff --git a/tools/cabana/videowidget.cc b/openpilot/tools/cabana/videowidget.cc similarity index 95% rename from tools/cabana/videowidget.cc rename to openpilot/tools/cabana/videowidget.cc index 87d3cbec95..67b89cb080 100644 --- a/tools/cabana/videowidget.cc +++ b/openpilot/tools/cabana/videowidget.cc @@ -157,7 +157,7 @@ QWidget *VideoWidget::createCameraWidget() { slider->setTimeRange(can->minSeconds(), can->maxSeconds()); QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->currentSecond()); }); - QObject::connect(can, &AbstractStream::paused, cam_widget, [c = cam_widget]() { c->showPausedOverlay(); }); + QObject::connect(can, &AbstractStream::paused, cam_widget, qOverload<>(&StreamCameraView::update)); QObject::connect(can, &AbstractStream::eventsMerged, this, [this]() { slider->update(); }); QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); }); QObject::connect(cam_widget, &CameraWidget::vipcAvailableStreamsUpdated, this, &VideoWidget::vipcAvailableStreamsUpdated); @@ -203,7 +203,7 @@ void VideoWidget::timeRangeChanged() { QString VideoWidget::formatTime(double sec, bool include_milliseconds) { if (settings.absolute_time) - sec = can->beginDateTime().addMSecs(sec * 1000).toMSecsSinceEpoch() / 1000.0; + sec += std::chrono::duration(can->beginDateTime().time_since_epoch()).count(); return utils::formatSeconds(sec, include_milliseconds, settings.absolute_time); } @@ -324,12 +324,6 @@ void Slider::mousePressEvent(QMouseEvent *e) { // StreamCameraView StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent) : CameraWidget(stream_name, stream_type, parent) { - fade_animation = new QPropertyAnimation(this, "overlayOpacity"); - fade_animation->setDuration(500); - fade_animation->setStartValue(0.2f); - fade_animation->setEndValue(0.7f); - fade_animation->setEasingCurve(QEasingCurve::InOutQuad); - connect(fade_animation, &QPropertyAnimation::valueChanged, this, QOverload<>::of(&StreamCameraView::update)); } void StreamCameraView::parseQLog(std::shared_ptr qlog) { @@ -362,8 +356,8 @@ void StreamCameraView::parseQLog(std::shared_ptr qlog) { update(); } -void StreamCameraView::paintGL() { - CameraWidget::paintGL(); +void StreamCameraView::paintEvent(QPaintEvent *event) { + CameraWidget::paintEvent(event); QPainter p(this); bool scrubbing = false; @@ -376,7 +370,7 @@ void StreamCameraView::paintGL() { } if (can->isPaused()) { - p.setPen(QColor(200, 200, 200, static_cast(255 * fade_animation->currentValue().toFloat()))); + p.setPen(QColor(200, 200, 200, static_cast(255 * 0.7f))); p.setFont(QFont(font().family(), 16, QFont::Bold)); p.drawText(rect(), Qt::AlignCenter, tr("PAUSED")); } diff --git a/tools/cabana/videowidget.h b/openpilot/tools/cabana/videowidget.h similarity index 93% rename from tools/cabana/videowidget.h rename to openpilot/tools/cabana/videowidget.h index e52e92ebd1..09f7a9931b 100644 --- a/tools/cabana/videowidget.h +++ b/openpilot/tools/cabana/videowidget.h @@ -7,7 +7,6 @@ #include #include -#include #include #include #include @@ -36,8 +35,7 @@ class StreamCameraView : public CameraWidget { public: StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent = nullptr); - void paintGL() override; - void showPausedOverlay() { fade_animation->start(); } + void paintEvent(QPaintEvent *event) override; void parseQLog(std::shared_ptr qlog); private: @@ -47,7 +45,6 @@ private: void drawScrubThumbnail(QPainter &p); void drawTime(QPainter &p, const QRect &rect, double seconds); - QPropertyAnimation *fade_animation; std::map big_thumbnails; std::map thumbnails; double thumbnail_dispaly_time = -1; diff --git a/tools/camerastream/README.md b/openpilot/tools/camerastream/README.md similarity index 74% rename from tools/camerastream/README.md rename to openpilot/tools/camerastream/README.md index 2f7498a07c..9671199297 100644 --- a/tools/camerastream/README.md +++ b/openpilot/tools/camerastream/README.md @@ -7,7 +7,7 @@ ### On the device SSH into the device and run following in separate terminals: -`cd /data/openpilot/cereal/messaging && ./bridge` +`cd /data/openpilot && ./openpilot/cereal/messaging/bridge` `cd /data/openpilot/system/loggerd && ./encoderd` @@ -18,8 +18,8 @@ Note that both the device and your PC must be on the same openpilot commit. Alternatively paste this as a single command: ``` ( - cd /data/openpilot/cereal/messaging/ - ./bridge & + cd /data/openpilot + ./openpilot/cereal/messaging/bridge & cd /data/openpilot/system/camerad/ ./camerad & @@ -44,18 +44,18 @@ To actually display the stream, run `watch3` in separate terminal: ## compressed_vipc.py usage ``` $ python3 compressed_vipc.py -h -usage: compressed_vipc.py [-h] [--nvidia] [--cams CAMS] [--silent] addr +usage: compressed_vipc.py [-h] [--cams CAMS] [--server SERVER] [--silent] addr Decode video streams and broadcast on VisionIPC positional arguments: - addr Address of comma three + addr Address of comma three options: - -h, --help show this help message and exit - --nvidia Use nvidia instead of ffmpeg - --cams CAMS Cameras to decode - --silent Suppress debug output + -h, --help show this help message and exit + --cams CAMS Cameras to decode + --server SERVER choose vipc server name + --silent Suppress debug output ``` diff --git a/tools/camerastream/compressed_vipc.py b/openpilot/tools/camerastream/compressed_vipc.py similarity index 60% rename from tools/camerastream/compressed_vipc.py rename to openpilot/tools/camerastream/compressed_vipc.py index 4dc74272ea..56ed12889f 100755 --- a/tools/camerastream/compressed_vipc.py +++ b/openpilot/tools/camerastream/compressed_vipc.py @@ -1,16 +1,15 @@ #!/usr/bin/env python3 -import av import os -import sys import argparse -import numpy as np import multiprocessing import time import signal +from collections import deque -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from msgq.visionipc import VisionIpcServer, VisionStreamType +from openpilot.tools.camerastream.ffmpeg_decoder import Decoder, FFmpegError V4L2_BUF_FLAG_KEYFRAME = 8 @@ -24,23 +23,12 @@ ENCODE_SOCKETS = { VisionStreamType.VISION_STREAM_WIDE_ROAD: "wideRoadEncodeData", } -def decoder(addr, vipc_server, vst, nvidia, W, H, debug=False): +def decoder(addr, vipc_server, vst, W, H, debug=False): sock_name = ENCODE_SOCKETS[vst] if debug: print(f"start decoder for {sock_name}, {W}x{H}") - if nvidia: - os.environ["NV_LOW_LATENCY"] = "3" # both bLowLatency and CUVID_PKT_ENDOFPICTURE - sys.path += os.environ["LD_LIBRARY_PATH"].split(":") - import PyNvCodec as nvc - - nvDec = nvc.PyNvDecoder(W, H, nvc.PixelFormat.NV12, nvc.CudaVideoCodec.HEVC, 0) - cc1 = nvc.ColorspaceConversionContext(nvc.ColorSpace.BT_709, nvc.ColorRange.JPEG) - conv_yuv = nvc.PySurfaceConverter(W, H, nvc.PixelFormat.NV12, nvc.PixelFormat.YUV420, 0) - nvDwn_yuv = nvc.PySurfaceDownloader(W, H, nvc.PixelFormat.YUV420, 0) - img_yuv = np.ndarray((H*W//2*3), dtype=np.uint8) - else: - codec = av.CodecContext.create("hevc", "r") + codec = Decoder("hevc") os.environ["ZMQ"] = "1" messaging.reset_context() @@ -49,13 +37,22 @@ def decoder(addr, vipc_server, vst, nvidia, W, H, debug=False): last_idx = -1 seen_iframe = False - time_q = [] + time_q = deque() + + def resync(): + nonlocal seen_iframe + codec.reset() + seen_iframe = False + time_q.clear() + while 1: msgs = messaging.drain_sock(sock, wait_for_one=True) for evt in msgs: evta = getattr(evt, evt.which()) - if debug and evta.idx.encodeId != 0 and evta.idx.encodeId != (last_idx+1): - print("DROP PACKET!") + if last_idx != -1 and evta.idx.encodeId != (last_idx + 1): + if debug: + print("DROP PACKET!") + resync() last_idx = evta.idx.encodeId if not seen_iframe and not (evta.idx.flags & V4L2_BUF_FLAG_KEYFRAME): if debug: @@ -66,48 +63,48 @@ def decoder(addr, vipc_server, vst, nvidia, W, H, debug=False): frame_latency = ((evta.idx.timestampEof/1e9) - (evta.idx.timestampSof/1e9))*1000 process_latency = ((evt.logMonoTime/1e9) - (evta.idx.timestampEof/1e9))*1000 - # put in header (first) + # put in header (first) — VPS/SPS/PPS only, no frame expected if not seen_iframe: - if nvidia: - nvDec.DecodeSurfaceFromPacket(np.frombuffer(evta.header, dtype=np.uint8)) - else: - codec.decode(av.packet.Packet(evta.header)) + try: + codec.decode(evta.header) + except FFmpegError as e: + if debug: + print(f"HEADER ERROR: {e}") + resync() + continue seen_iframe = True - if nvidia: - rawSurface = nvDec.DecodeSurfaceFromPacket(np.frombuffer(evta.data, dtype=np.uint8)) - if rawSurface.Empty(): - if debug: - print("DROP SURFACE") - continue - convSurface = conv_yuv.Execute(rawSurface, cc1) - nvDwn_yuv.DownloadSingleSurface(convSurface, img_yuv) - else: - frames = codec.decode(av.packet.Packet(evta.data)) - if len(frames) == 0: - if debug: - print("DROP SURFACE") - continue - assert len(frames) == 1 - img_yuv = frames[0].to_ndarray(format=av.video.format.VideoFormat('yuv420p')).flatten() - uv_offset = H*W - y = img_yuv[:uv_offset] - uv = img_yuv[uv_offset:].reshape(2, -1).ravel('F') - img_yuv = np.hstack((y, uv)) + try: + img_yuv = codec.decode(evta.data) + except FFmpegError as e: + if debug: + print(f"DECODE ERROR: {e}") + resync() + continue - vipc_server.send(vst, img_yuv.data, cnt, int(time_q[0]*1e9), int(time.monotonic()*1e9)) + if img_yuv is None: + if debug: + print("DROP SURFACE") + continue + + if codec.width != W or codec.height != H: + if debug: + print(f"DECODE ERROR: decoded frame is {codec.width}x{codec.height}, expected {W}x{H}") + resync() + continue + + frame_start_time = time_q.popleft() + vipc_server.send(vst, img_yuv.data, cnt, int(frame_start_time*1e9), int(time.monotonic()*1e9)) cnt += 1 - pc_latency = (time.monotonic()-time_q[0])*1000 - time_q = time_q[1:] + pc_latency = (time.monotonic()-frame_start_time)*1000 if debug: print(f"{len(msgs):2d} {evta.idx.encodeId:4d} {evt.logMonoTime/1e9:.3f} {evta.idx.timestampEof/1e6:.3f} \ roll {frame_latency:6.2f} ms latency {process_latency:6.2f} ms + {network_latency:6.2f} ms + {pc_latency:6.2f} ms \ = {process_latency+network_latency+pc_latency:6.2f} ms", len(evta.data), sock_name) - class CompressedVipc: - def __init__(self, addr, vision_streams, server_name, nvidia=False, debug=False): + def __init__(self, addr, vision_streams, server_name, debug=False): print("getting frame sizes") os.environ["ZMQ"] = "1" messaging.reset_context() @@ -126,7 +123,7 @@ class CompressedVipc: self.procs = [] for vst in vision_streams: ed = sm[ENCODE_SOCKETS[vst]] - p = multiprocessing.Process(target=decoder, args=(addr, self.vipc_server, vst, nvidia, ed.width, ed.height, debug)) + p = multiprocessing.Process(target=decoder, args=(addr, self.vipc_server, vst, ed.width, ed.height, debug)) p.start() self.procs.append(p) @@ -142,7 +139,6 @@ class CompressedVipc: if __name__ == "__main__": parser = argparse.ArgumentParser(description="Decode video streams and broadcast on VisionIPC") parser.add_argument("addr", help="Address of comma three") - parser.add_argument("--nvidia", action="store_true", help="Use nvidia instead of ffmpeg") parser.add_argument("--cams", default="0,1,2", help="Cameras to decode") parser.add_argument("--server", default="camerad", help="choose vipc server name") parser.add_argument("--silent", action="store_true", help="Suppress debug output") @@ -155,7 +151,7 @@ if __name__ == "__main__": ] vsts = [vision_streams[int(x)] for x in args.cams.split(",")] - cvipc = CompressedVipc(args.addr, vsts, args.server, args.nvidia, debug=(not args.silent)) + cvipc = CompressedVipc(args.addr, vsts, args.server, debug=(not args.silent)) # register exit handler signal.signal(signal.SIGINT, lambda sig, frame: cvipc.kill()) diff --git a/openpilot/tools/camerastream/ffmpeg_decoder.py b/openpilot/tools/camerastream/ffmpeg_decoder.py new file mode 100644 index 0000000000..13cdc12df1 --- /dev/null +++ b/openpilot/tools/camerastream/ffmpeg_decoder.py @@ -0,0 +1,261 @@ +import ctypes +import errno +import os + +import ffmpeg +import numpy as np + + +AV_INPUT_BUFFER_PADDING_SIZE = 64 +AV_LOG_QUIET = -8 +SWS_FAST_BILINEAR = 1 + + +class FFmpegError(RuntimeError): + pass + + +class AVPacket(ctypes.Structure): + # Public prefix of AVPacket. Only data and size are modified here; the packet + # remains non-refcounted and points at Decoder._packet_buffer. + _fields_ = [ + ("buf", ctypes.c_void_p), + ("pts", ctypes.c_int64), + ("dts", ctypes.c_int64), + ("data", ctypes.POINTER(ctypes.c_uint8)), + ("size", ctypes.c_int), + ] + + +class AVFrame(ctypes.Structure): + # Public prefix of AVFrame through format. Stable within a libavutil major + _fields_ = [ + ("data", ctypes.POINTER(ctypes.c_uint8) * 8), + ("linesize", ctypes.c_int * 8), + ("extended_data", ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8))), + ("width", ctypes.c_int), + ("height", ctypes.c_int), + ("nb_samples", ctypes.c_int), + ("format", ctypes.c_int), + ] + + +def _bind(fn, restype, *argtypes): + fn.restype = restype + fn.argtypes = list(argtypes) + return fn + + +def _load_libraries(): + avutil = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libavutil.so.59"), mode=ctypes.RTLD_GLOBAL) + avcodec = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libavcodec.so.61"), mode=ctypes.RTLD_GLOBAL) + swscale = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libswscale.so.8"), mode=ctypes.RTLD_GLOBAL) + + c_int, c_char_p, c_void_p, c_size_t = ctypes.c_int, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_size_t + c_uint8_p = ctypes.POINTER(ctypes.c_uint8) + c_void_p_p = ctypes.POINTER(c_void_p) + + _bind(avutil.av_log_set_level, None, c_int) + _bind(avutil.av_opt_set, c_int, c_void_p, c_char_p, c_char_p, c_int) + _bind(avutil.av_strerror, c_int, c_int, c_char_p, c_size_t) + _bind(avutil.av_get_pix_fmt, c_int, c_char_p) + + _bind(avcodec.avcodec_find_decoder_by_name, c_void_p, c_char_p) + _bind(avcodec.avcodec_alloc_context3, c_void_p, c_void_p) + _bind(avcodec.avcodec_open2, c_int, c_void_p, c_void_p, c_void_p) + _bind(avcodec.avcodec_free_context, None, c_void_p_p) + _bind(avcodec.avcodec_flush_buffers, None, c_void_p) + _bind(avcodec.avcodec_send_packet, c_int, c_void_p, ctypes.POINTER(AVPacket)) + _bind(avcodec.avcodec_receive_frame, c_int, c_void_p, ctypes.POINTER(AVFrame)) + _bind(avcodec.av_packet_alloc, ctypes.POINTER(AVPacket)) + _bind(avcodec.av_packet_free, None, ctypes.POINTER(ctypes.POINTER(AVPacket))) + _bind(avcodec.av_frame_alloc, ctypes.POINTER(AVFrame)) + _bind(avcodec.av_frame_free, None, ctypes.POINTER(ctypes.POINTER(AVFrame))) + _bind(avcodec.av_frame_unref, None, ctypes.POINTER(AVFrame)) + + _bind(swscale.sws_getCachedContext, c_void_p, + c_void_p, c_int, c_int, c_int, c_int, c_int, c_int, c_int, c_void_p, c_void_p, c_void_p) + _bind(swscale.sws_scale, c_int, + c_void_p, ctypes.POINTER(c_uint8_p), ctypes.POINTER(c_int), + c_int, c_int, ctypes.POINTER(c_uint8_p), ctypes.POINTER(c_int)) + _bind(swscale.sws_freeContext, None, c_void_p) + + avutil.av_log_set_level(AV_LOG_QUIET) + return avutil, avcodec, swscale +_avutil, _avcodec, _swscale = _load_libraries() + +_DataArray = ctypes.POINTER(ctypes.c_uint8) * 4 +_LinesizeArray = ctypes.c_int * 4 + +AV_PIX_FMT_NV12 = _avutil.av_get_pix_fmt(b"nv12") +assert AV_PIX_FMT_NV12 >= 0 + + +def _error_string(code: int) -> str: + buf = ctypes.create_string_buffer(256) + if _avutil.av_strerror(code, buf, len(buf)) == 0: + return buf.value.decode(errors="replace") + return f"FFmpeg error {code}" + + +def _check(code: int, operation: str) -> None: + if code < 0: + raise FFmpegError(f"{operation}: {_error_string(code)}") + + +class Decoder: + def __init__(self, codec_name: str = "hevc"): + self.closed = True + self._sws_context = ctypes.c_void_p() + self._packet_buffer = bytearray() + self._packet_address = 0 + self._packet_data = None + self._output = np.empty(0, dtype=np.uint8) + self._dst_data = _DataArray() + self._dst_linesize = _LinesizeArray() + self.width = 0 + self.height = 0 + self._src_format = -1 + + codec = _avcodec.avcodec_find_decoder_by_name(codec_name.encode()) + if not codec: + raise FFmpegError(f"decoder not found: {codec_name}") + + self._context = ctypes.c_void_p(_avcodec.avcodec_alloc_context3(codec)) + if not self._context: + raise MemoryError("avcodec_alloc_context3 failed") + + self._packet = _avcodec.av_packet_alloc() + if not self._packet: + _avcodec.avcodec_free_context(ctypes.byref(self._context)) + raise MemoryError("av_packet_alloc failed") + + self._frame = _avcodec.av_frame_alloc() + if not self._frame: + _avcodec.av_packet_free(ctypes.byref(self._packet)) + _avcodec.avcodec_free_context(ctypes.byref(self._context)) + raise MemoryError("av_frame_alloc failed") + + try: + # Frame threading holds decoded frames to populate worker pipelines. + # Slice threads can reduce decode time without adding that frame queue; + # four was the latency minimum on the replay camera workload. + _check(_avutil.av_opt_set(self._context, b"threads", b"4", 0), "set decoder threads") + _check(_avutil.av_opt_set(self._context, b"thread_type", b"slice", 0), "set decoder thread type") + _check(_avutil.av_opt_set(self._context, b"flags", b"+low_delay", 0), "set low-delay mode") + _check(_avcodec.avcodec_open2(self._context, codec, None), "open decoder") + except Exception: + _avcodec.av_frame_free(ctypes.byref(self._frame)) + _avcodec.av_packet_free(ctypes.byref(self._packet)) + _avcodec.avcodec_free_context(ctypes.byref(self._context)) + raise + + self.closed = False + + def __enter__(self): + self._ensure_open() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def _ensure_open(self) -> None: + if self.closed: + raise RuntimeError("decoder is closed") + + def _prepare_packet(self, data) -> None: + size = len(data) + required = size + AV_INPUT_BUFFER_PADDING_SIZE + if len(self._packet_buffer) < required: + # Grow-only; the address stays valid until the next reallocation. + self._packet_buffer = bytearray(required) + self._packet_address = ctypes.addressof(ctypes.c_uint8.from_buffer(self._packet_buffer)) + self._packet_data = ctypes.cast(self._packet_address, ctypes.POINTER(ctypes.c_uint8)) + + self._packet_buffer[:size] = data + ctypes.memset(self._packet_address + size, 0, AV_INPUT_BUFFER_PADDING_SIZE) + self._packet.contents.data = self._packet_data + self._packet.contents.size = size + + def _prepare_output(self, frame: AVFrame) -> None: + width, height, src_format = frame.width, frame.height, frame.format + if width <= 0 or height <= 0 or width % 2 or height % 2: + raise FFmpegError(f"unsupported frame dimensions: {width}x{height}") + if (width, height, src_format) == (self.width, self.height, self._src_format): + return + + sws_context = _swscale.sws_getCachedContext( + self._sws_context, width, height, src_format, + width, height, AV_PIX_FMT_NV12, SWS_FAST_BILINEAR, + None, None, None, + ) + if not sws_context: + raise FFmpegError("sws_getCachedContext failed") + self._sws_context = ctypes.c_void_p(sws_context) + + self.width, self.height = width, height + self._src_format = src_format + self._output = np.empty(width * height * 3 // 2, dtype=np.uint8) + output_address = self._output.ctypes.data + self._dst_data = _DataArray( + ctypes.cast(output_address, ctypes.POINTER(ctypes.c_uint8)), + ctypes.cast(output_address + width * height, ctypes.POINTER(ctypes.c_uint8)), + None, + None, + ) + self._dst_linesize = _LinesizeArray(width, width, 0, 0) + + def _receive(self) -> np.ndarray | None: + """Return one NV12 frame, or None if the decoder needs more input. + + The returned buffer is reused on the next successful decode; callers must + use or copy it before calling decode again. + """ + result = _avcodec.avcodec_receive_frame(self._context, self._frame) + if result == -errno.EAGAIN: + return None + _check(result, "receive decoded frame") + + try: + frame = self._frame.contents + self._prepare_output(frame) + rows = _swscale.sws_scale( + self._sws_context, frame.data, frame.linesize, 0, frame.height, + self._dst_data, self._dst_linesize, + ) + if rows != frame.height: + raise FFmpegError(f"convert decoded frame: produced {rows} of {frame.height} rows") + return self._output + finally: + _avcodec.av_frame_unref(self._frame) + + def decode(self, data) -> np.ndarray | None: + self._ensure_open() + if len(data) == 0: + return None + + self._prepare_packet(data) + result = _avcodec.avcodec_send_packet(self._context, self._packet) + # The packet buffer is ours, not FFmpeg's. Clear the borrowed pointer so + # packet teardown can never attempt to release it. + self._packet.contents.data = None + self._packet.contents.size = 0 + _check(result, "send packet to decoder") + return self._receive() + + def reset(self) -> None: + """Discard decoder state after a stream discontinuity.""" + self._ensure_open() + _avcodec.avcodec_flush_buffers(self._context) + + def close(self) -> None: + if self.closed: + return + self.closed = True + _swscale.sws_freeContext(self._sws_context) + _avcodec.av_frame_free(ctypes.byref(self._frame)) + _avcodec.av_packet_free(ctypes.byref(self._packet)) + _avcodec.avcodec_free_context(ctypes.byref(self._context)) + + def __del__(self): + self.close() diff --git a/tools/clip/run.py b/openpilot/tools/clip/run.py similarity index 94% rename from tools/clip/run.py rename to openpilot/tools/clip/run.py index d3a0496b1e..594383f7fd 100755 --- a/tools/clip/run.py +++ b/openpilot/tools/clip/run.py @@ -11,6 +11,7 @@ import itertools import numpy as np import tqdm from argparse import ArgumentParser +from collections.abc import Callable from pathlib import Path from concurrent.futures import ThreadPoolExecutor, as_completed @@ -138,7 +139,7 @@ def iter_segment_frames(camera_paths, start_time, end_time, fps=20, use_qcam=Fal 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 + get_frame: Callable[[int], 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 @@ -157,12 +158,12 @@ def iter_segment_frames(camera_paths, start_time, end_time, fps=20, use_qcam=Fal 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) + get_frame = seg_frames.__getitem__ else: - seg_frames = FrameReader(path, pix_fmt="nv12") + get_frame = FrameReader(path, pix_fmt="nv12").get - assert seg_frames is not None - frame = seg_frames[local_idx] if use_qcam else seg_frames.get(local_idx) - yield global_idx, frame + assert get_frame is not None + yield global_idx, get_frame(local_idx) class FrameQueue: @@ -312,8 +313,15 @@ def clip(route: Route, output: str, start: int, end: int, headless: bool = 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) + ecamera_paths = route.ecamera_paths() if not use_qcam else [] + wide_frame_queue: FrameQueue | None = None + if any(p for p in ecamera_paths[seg_start:seg_end] if p): + wide_frame_queue = FrameQueue(ecamera_paths, start, end, fps=FRAMERATE) + vipc = VisionIpcServer("camerad") vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 4, frame_queue.frame_w, frame_queue.frame_h) + if wide_frame_queue: + vipc.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 4, wide_frame_queue.frame_w, wide_frame_queue.frame_h) vipc.start_listener() patch_submaster(message_chunks, ui_state) @@ -331,6 +339,9 @@ def clip(route: Route, output: str, start: int, end: int, headless: bool = True, break _, frame_bytes = frame_queue.get() vipc.send(VisionStreamType.VISION_STREAM_ROAD, frame_bytes, frame_idx, int(frame_idx * 5e7), int(frame_idx * 5e7)) + if wide_frame_queue: + _, wide_bytes = wide_frame_queue.get() + vipc.send(VisionStreamType.VISION_STREAM_WIDE_ROAD, wide_bytes, frame_idx, int(frame_idx * 5e7), int(frame_idx * 5e7)) ui_state.update() if should_render: road_view.render() @@ -340,6 +351,8 @@ def clip(route: Route, output: str, start: int, end: int, headless: bool = True, timer.lap("render") frame_queue.stop() + if wide_frame_queue: + wide_frame_queue.stop() gui_app.close() timer.lap("ffmpeg") diff --git a/tools/jotpluggler/.gitignore b/openpilot/tools/jotpluggler/.gitignore similarity index 100% rename from tools/jotpluggler/.gitignore rename to openpilot/tools/jotpluggler/.gitignore diff --git a/tools/jotpluggler/SConscript b/openpilot/tools/jotpluggler/SConscript similarity index 89% rename from tools/jotpluggler/SConscript rename to openpilot/tools/jotpluggler/SConscript index 078e173959..d5ebaffb98 100644 --- a/tools/jotpluggler/SConscript +++ b/openpilot/tools/jotpluggler/SConscript @@ -9,7 +9,7 @@ from opendbc.car.fingerprints import MIGRATION from opendbc.car.values import PLATFORMS from openpilot.common.basedir import BASEDIR -Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'replay_lib') +Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'replay_lib', 'ffmpeg_libs') jot_env = env.Clone() jot_env["LIBPATH"] += [imgui.MESA_DIR, libusb.LIB_DIR] @@ -83,7 +83,7 @@ def write_car_fingerprint_to_dbc_header(target, source, env): def generate_event_extractors(target, source, env): subprocess.check_call([ "python3", - "tools/jotpluggler/generate_event_extractors.py", + "openpilot/tools/jotpluggler/generate_event_extractors.py", os.path.realpath(BASEDIR), str(target[0]), ]) @@ -93,18 +93,20 @@ generated_dbc_stamp = jot_env.Command(f"generated_dbcs/.stamp", [], materialize_ car_fingerprint_to_dbc = jot_env.Command("car_fingerprint_to_dbc.h", [], write_car_fingerprint_to_dbc_header) event_extractors = jot_env.Command("generated_event_extractors.h", [ "generate_event_extractors.py", - jot_env.Glob("#cereal/*.capnp"), - jot_env.Glob("#cereal/include/*.capnp"), + jot_env.Glob("#openpilot/cereal/*.capnp"), + jot_env.Glob("#openpilot/cereal/include/*.capnp"), + "#opendbc_repo/opendbc/car/car.capnp", + "#opendbc_repo/opendbc/car/include/c++.capnp", ], generate_event_extractors, ) -libs = [replay_lib, common, messaging, visionipc, cereal, File(f"{imgui.LIB_DIR}/libimgui.a"), File(f"{imgui.LIB_DIR}/libglfw3.a"), - "avformat", "avcodec", "avutil", "x264", "yuv", "z", "bz2", "zstd", "m", "pthread", "usb-1.0"] +libs = [replay_lib, common, messaging, visionipc, cereal, File(f"{imgui.LIB_DIR}/libimgui.a"), File(f"{imgui.LIB_DIR}/libglfw3.a")] + \ + ffmpeg_libs + ["bz2", "zstd", "m", "pthread", "usb-1.0"] if arch == "Darwin": jot_env["FRAMEWORKS"] = ["OpenGL", "Cocoa", "IOKit", "CoreFoundation", "CoreVideo", "CoreMedia", "VideoToolbox"] else: - libs += ["GL", "dl", "va", "va-drm", "drm"] + libs += ["GL", "dl"] program = jot_env.Program("jotpluggler", jot_env.Glob("*.cc"), LIBS=libs) jot_env.Depends(program, generated_dbc_stamp) diff --git a/tools/jotpluggler/app.cc b/openpilot/tools/jotpluggler/app.cc similarity index 99% rename from tools/jotpluggler/app.cc rename to openpilot/tools/jotpluggler/app.cc index 22a9f9021c..e6ba696bae 100644 --- a/tools/jotpluggler/app.cc +++ b/openpilot/tools/jotpluggler/app.cc @@ -3,7 +3,8 @@ #include "tools/jotpluggler/common.h" #include "tools/jotpluggler/internal.h" #include "tools/jotpluggler/map.h" -#include "system/hardware/hw.h" +#include "tools/jotpluggler/thumbnail.h" +#include "common/hardware/hw.h" #include "imgui_impl_glfw.h" #include "imgui_internal.h" @@ -49,7 +50,7 @@ std::string layout_name_from_arg(const std::string &layout_arg) { } fs::path layouts_dir() { - return repo_root() / "tools" / "jotpluggler" / "layouts"; + return repo_root() / "openpilot" / "tools" / "jotpluggler" / "layouts"; } std::string sanitize_layout_stem(std::string_view name) { @@ -249,7 +250,7 @@ void configure_style() { g_ui_font = nullptr; g_ui_bold_font = nullptr; g_mono_font = nullptr; - const fs::path fonts_dir = repo_root() / "selfdrive" / "assets" / "fonts"; + const fs::path fonts_dir = repo_root() / "openpilot" / "selfdrive" / "assets" / "fonts"; ImFontConfig font_cfg; font_cfg.OversampleH = 2; font_cfg.OversampleV = 2; @@ -1033,10 +1034,12 @@ bool apply_special_item_to_pane(WorkspaceTab *tab, TabUiState *tab_state, int pa if (pane.title == UNTITLED_PANE_TITLE || previous_kind != PaneKind::Plot) { pane.title = spec->label; } - } else { + } else if (spec->kind == PaneKind::Camera) { pane.title = spec->label; resize_tab_pane_state(tab_state, tab->panes.size()); tab_state->camera_panes[static_cast(pane_index)].fit_to_pane = true; + } else { + pane.title = spec->label; } tab_state->active_pane_index = pane_index; return true; @@ -1565,6 +1568,8 @@ void draw_pane_windows(AppSession *session, UiState *state) { } if (pane.kind == PaneKind::Map) { draw_map_pane(session, state, &pane, static_cast(i)); + } else if (pane.kind == PaneKind::Thumbnail) { + draw_thumbnail_pane(session, state); } else if (pane.kind == PaneKind::Camera) { draw_camera_pane(session, state, tab_state, static_cast(i), pane); } else { @@ -1847,6 +1852,7 @@ int run(const Options &options) { for (std::unique_ptr &feed : session.pane_camera_feeds) { feed = std::make_unique(); } + session.thumbnail_view = std::make_unique(); sync_camera_feeds(&session); if (session.async_route_loading) { @@ -1892,6 +1898,7 @@ int run(const Options &options) { for (std::unique_ptr &feed : session.pane_camera_feeds) { feed.reset(); } + session.thumbnail_view.reset(); return 0; } catch (const std::exception &err) { std::cerr << err.what() << "\n"; diff --git a/tools/jotpluggler/app.h b/openpilot/tools/jotpluggler/app.h similarity index 97% rename from tools/jotpluggler/app.h rename to openpilot/tools/jotpluggler/app.h index 872a6973d7..b7754f51b2 100644 --- a/tools/jotpluggler/app.h +++ b/openpilot/tools/jotpluggler/app.h @@ -1,6 +1,6 @@ #pragma once -#include "cereal/gen/cpp/log.capnp.h" +#include "openpilot/cereal/gen/cpp/log.capnp.h" #include "imgui.h" #include "tools/jotpluggler/dbc.h" #include "tools/jotpluggler/util.h" @@ -81,6 +81,7 @@ struct Curve { enum class PaneKind : uint8_t { Plot, Map, + Thumbnail, Camera, }; @@ -141,9 +142,15 @@ struct CameraFeedIndex { std::vector entries; }; +struct ThumbnailFrame { + double timestamp = 0.0; + int segment = -1; + std::vector jpeg; +}; + enum class LogOrigin : uint8_t { Log, - Android, + OperatingSystem, Alert, }; @@ -318,6 +325,7 @@ struct RouteData { CameraFeedIndex driver_camera; CameraFeedIndex wide_road_camera; CameraFeedIndex qroad_camera; + std::vector thumbnails; GpsTrace gps_trace; std::vector logs; std::vector timeline; @@ -445,6 +453,7 @@ bool icon_menu_item(const char *glyph, class AsyncRouteLoader; class CameraFeedView; +class ThumbnailView; class StreamPoller; class MapDataManager; @@ -486,6 +495,7 @@ struct AppSession { std::unique_ptr route_loader; std::unique_ptr stream_poller; std::array, 4> pane_camera_feeds; + std::unique_ptr thumbnail_view; std::unique_ptr map_data; bool async_route_loading = false; double next_stream_custom_refresh_time = 0.0; @@ -885,3 +895,20 @@ private: struct Impl; std::unique_ptr impl_; }; + +class ThumbnailView { +public: + ThumbnailView(); + ~ThumbnailView(); + + ThumbnailView(const ThumbnailView &) = delete; + ThumbnailView &operator=(const ThumbnailView &) = delete; + + void setThumbnails(const std::vector &thumbnails); + void update(double tracker_time); + void drawSized(ImVec2 size, bool loading); + +private: + struct Impl; + std::unique_ptr impl_; +}; diff --git a/tools/jotpluggler/browser.cc b/openpilot/tools/jotpluggler/browser.cc similarity index 100% rename from tools/jotpluggler/browser.cc rename to openpilot/tools/jotpluggler/browser.cc diff --git a/tools/jotpluggler/camera.cc b/openpilot/tools/jotpluggler/camera.cc similarity index 100% rename from tools/jotpluggler/camera.cc rename to openpilot/tools/jotpluggler/camera.cc diff --git a/tools/jotpluggler/camera.h b/openpilot/tools/jotpluggler/camera.h similarity index 100% rename from tools/jotpluggler/camera.h rename to openpilot/tools/jotpluggler/camera.h diff --git a/tools/jotpluggler/common.cc b/openpilot/tools/jotpluggler/common.cc similarity index 97% rename from tools/jotpluggler/common.cc rename to openpilot/tools/jotpluggler/common.cc index 8f696657bd..50f5fc0b95 100644 --- a/tools/jotpluggler/common.cc +++ b/openpilot/tools/jotpluggler/common.cc @@ -46,11 +46,11 @@ const char *special_item_label(std::string_view item_id) { } bool pane_kind_is_special(PaneKind kind) { - return kind == PaneKind::Map || kind == PaneKind::Camera; + return kind == PaneKind::Map || kind == PaneKind::Thumbnail || kind == PaneKind::Camera; } bool is_default_special_title(std::string_view title) { - if (title == "Map") return true; + if (title == "Map" || title == "Thumbnail") return true; return std::any_of(kCameraViewSpecs.begin(), kCameraViewSpecs.end(), [&](const CameraViewSpec &spec) { return title == spec.label; }); diff --git a/tools/jotpluggler/common.h b/openpilot/tools/jotpluggler/common.h similarity index 95% rename from tools/jotpluggler/common.h rename to openpilot/tools/jotpluggler/common.h index 14db83fd33..5c68b0ed22 100644 --- a/tools/jotpluggler/common.h +++ b/openpilot/tools/jotpluggler/common.h @@ -28,8 +28,9 @@ inline constexpr std::array kCameraViewSpecs = {{ {CameraViewKind::QRoad, "qRoad Camera", "qroad", "qroad", "camera_qroad", &RouteData::qroad_camera}, }}; -inline constexpr std::array kSpecialItemSpecs = {{ +inline constexpr std::array kSpecialItemSpecs = {{ {"map", "Map", PaneKind::Map, CameraViewKind::Road}, + {"thumbnail", "Thumbnail", PaneKind::Thumbnail, CameraViewKind::Road}, {kCameraViewSpecs[0].special_item_id, kCameraViewSpecs[0].label, PaneKind::Camera, kCameraViewSpecs[0].view}, {kCameraViewSpecs[1].special_item_id, kCameraViewSpecs[1].label, PaneKind::Camera, kCameraViewSpecs[1].view}, {kCameraViewSpecs[2].special_item_id, kCameraViewSpecs[2].label, PaneKind::Camera, kCameraViewSpecs[2].view}, diff --git a/tools/jotpluggler/custom_series.cc b/openpilot/tools/jotpluggler/custom_series.cc similarity index 99% rename from tools/jotpluggler/custom_series.cc rename to openpilot/tools/jotpluggler/custom_series.cc index aabf78a0db..70b3e8c3cb 100644 --- a/tools/jotpluggler/custom_series.cc +++ b/openpilot/tools/jotpluggler/custom_series.cc @@ -189,7 +189,7 @@ PythonEvalResult evaluate_custom_python_series(const AppSession &session, const CommandResult process = run_process_capture_output({ "python3", - (repo_root() / "tools" / "jotpluggler" / "math_eval.py").string(), + (repo_root() / "openpilot" / "tools" / "jotpluggler" / "math_eval.py").string(), manifest_path.string(), globals_path.string(), code_path.string(), diff --git a/tools/jotpluggler/dbc.h b/openpilot/tools/jotpluggler/dbc.h similarity index 100% rename from tools/jotpluggler/dbc.h rename to openpilot/tools/jotpluggler/dbc.h diff --git a/tools/jotpluggler/generate_event_extractors.py b/openpilot/tools/jotpluggler/generate_event_extractors.py similarity index 98% rename from tools/jotpluggler/generate_event_extractors.py rename to openpilot/tools/jotpluggler/generate_event_extractors.py index 1aba9578cd..be9bd49003 100644 --- a/tools/jotpluggler/generate_event_extractors.py +++ b/openpilot/tools/jotpluggler/generate_event_extractors.py @@ -275,7 +275,7 @@ class Generator: def generate(self): self.lines = [] - self.emit(0, "// Generated by tools/jotpluggler/generate_event_extractors.py; do not edit.") + self.emit(0, "// Generated by openpilot/tools/jotpluggler/generate_event_extractors.py; do not edit.") self.emit(0, "") self.emit(0, "const std::vector &static_event_fixed_paths() {") self.emit(2, "static const std::vector paths = {") @@ -321,7 +321,7 @@ if __name__ == "__main__": repo_root = Path(sys.argv[1]).resolve() output = Path(sys.argv[2]) capnp.remove_import_hook() - log = capnp.load(str(repo_root / "cereal" / "log.capnp")) + log = capnp.load(str(repo_root / "openpilot" / "cereal" / "log.capnp"), imports=[str(repo_root / "opendbc_repo" / "opendbc" / "car")]) generated = Generator(log.Event.schema).generate() output.parent.mkdir(parents=True, exist_ok=True) output.write_text(generated) diff --git a/tools/jotpluggler/generated_dbcs/.gitignore b/openpilot/tools/jotpluggler/generated_dbcs/.gitignore similarity index 100% rename from tools/jotpluggler/generated_dbcs/.gitignore rename to openpilot/tools/jotpluggler/generated_dbcs/.gitignore diff --git a/tools/jotpluggler/icons.cc b/openpilot/tools/jotpluggler/icons.cc similarity index 100% rename from tools/jotpluggler/icons.cc rename to openpilot/tools/jotpluggler/icons.cc diff --git a/tools/jotpluggler/internal.h b/openpilot/tools/jotpluggler/internal.h similarity index 100% rename from tools/jotpluggler/internal.h rename to openpilot/tools/jotpluggler/internal.h diff --git a/tools/jotpluggler/layout.cc b/openpilot/tools/jotpluggler/layout.cc similarity index 98% rename from tools/jotpluggler/layout.cc rename to openpilot/tools/jotpluggler/layout.cc index 0d3b82c254..6bc3a6168a 100644 --- a/tools/jotpluggler/layout.cc +++ b/openpilot/tools/jotpluggler/layout.cc @@ -1,5 +1,5 @@ #include "tools/jotpluggler/internal.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include @@ -274,9 +274,9 @@ std::string default_dbc_template() { } DbcEditorSource resolve_dbc_editor_source(const std::string &dbc_name) { - const fs::path generated_dbc_dir = repo_root() / "tools" / "jotpluggler" / "generated_dbcs"; + const fs::path generated_dbc_dir = repo_root() / "openpilot" / "tools" / "jotpluggler" / "generated_dbcs"; const std::array candidates = {{ - {.path = repo_root() / "opendbc" / "dbc" / (dbc_name + ".dbc"), .kind = DbcEditorState::SourceKind::Opendbc}, + {.path = repo_root() / "opendbc_repo" / "opendbc" / "dbc" / (dbc_name + ".dbc"), .kind = DbcEditorState::SourceKind::Opendbc}, {.path = generated_dbc_dir / (dbc_name + ".dbc"), .kind = DbcEditorState::SourceKind::Generated}, }}; for (const DbcEditorSource &candidate : candidates) { @@ -334,7 +334,7 @@ bool save_dbc_editor_contents(AppSession *session, UiState *state) { } try { dbc::Database::fromContent(editor.text, editor.save_name + ".dbc"); - const fs::path generated_dbc_dir = repo_root() / "tools" / "jotpluggler" / "generated_dbcs"; + const fs::path generated_dbc_dir = repo_root() / "openpilot" / "tools" / "jotpluggler" / "generated_dbcs"; fs::create_directories(generated_dbc_dir); const fs::path output = generated_dbc_dir / (editor.save_name + ".dbc"); write_file_or_throw(output, editor.text); diff --git a/tools/jotpluggler/layout_io.cc b/openpilot/tools/jotpluggler/layout_io.cc similarity index 98% rename from tools/jotpluggler/layout_io.cc rename to openpilot/tools/jotpluggler/layout_io.cc index 5c70f7a42a..24e3a4b51f 100644 --- a/tools/jotpluggler/layout_io.cc +++ b/openpilot/tools/jotpluggler/layout_io.cc @@ -62,6 +62,8 @@ json11::Json workspace_node_to_json(const WorkspaceNode &node, const WorkspaceTa }; if (pane.kind == PaneKind::Map) { obj["kind"] = "map"; + } else if (pane.kind == PaneKind::Thumbnail) { + obj["kind"] = "thumbnail"; } else if (pane.kind == PaneKind::Camera) { obj["kind"] = "camera"; obj["camera_view"] = camera_view_spec(pane.camera_view).layout_name; diff --git a/tools/jotpluggler/layouts/.gitignore b/openpilot/tools/jotpluggler/layouts/.gitignore similarity index 100% rename from tools/jotpluggler/layouts/.gitignore rename to openpilot/tools/jotpluggler/layouts/.gitignore diff --git a/tools/jotpluggler/layouts/CAN-bus-debug.json b/openpilot/tools/jotpluggler/layouts/CAN-bus-debug.json similarity index 100% rename from tools/jotpluggler/layouts/CAN-bus-debug.json rename to openpilot/tools/jotpluggler/layouts/CAN-bus-debug.json diff --git a/tools/jotpluggler/layouts/camera-timings.json b/openpilot/tools/jotpluggler/layouts/camera-timings.json similarity index 100% rename from tools/jotpluggler/layouts/camera-timings.json rename to openpilot/tools/jotpluggler/layouts/camera-timings.json diff --git a/tools/jotpluggler/layouts/cameras-and-map.json b/openpilot/tools/jotpluggler/layouts/cameras-and-map.json similarity index 100% rename from tools/jotpluggler/layouts/cameras-and-map.json rename to openpilot/tools/jotpluggler/layouts/cameras-and-map.json diff --git a/tools/jotpluggler/layouts/can-states.json b/openpilot/tools/jotpluggler/layouts/can-states.json similarity index 100% rename from tools/jotpluggler/layouts/can-states.json rename to openpilot/tools/jotpluggler/layouts/can-states.json diff --git a/tools/jotpluggler/layouts/controls_mismatch_debug.json b/openpilot/tools/jotpluggler/layouts/controls_mismatch_debug.json similarity index 100% rename from tools/jotpluggler/layouts/controls_mismatch_debug.json rename to openpilot/tools/jotpluggler/layouts/controls_mismatch_debug.json diff --git a/tools/jotpluggler/layouts/driver-monitoring-debug.json b/openpilot/tools/jotpluggler/layouts/driver-monitoring-debug.json similarity index 100% rename from tools/jotpluggler/layouts/driver-monitoring-debug.json rename to openpilot/tools/jotpluggler/layouts/driver-monitoring-debug.json diff --git a/tools/jotpluggler/layouts/gps.json b/openpilot/tools/jotpluggler/layouts/gps.json similarity index 100% rename from tools/jotpluggler/layouts/gps.json rename to openpilot/tools/jotpluggler/layouts/gps.json diff --git a/tools/jotpluggler/layouts/gps_vs_llk.json b/openpilot/tools/jotpluggler/layouts/gps_vs_llk.json similarity index 100% rename from tools/jotpluggler/layouts/gps_vs_llk.json rename to openpilot/tools/jotpluggler/layouts/gps_vs_llk.json diff --git a/tools/jotpluggler/layouts/locationd_debug.json b/openpilot/tools/jotpluggler/layouts/locationd_debug.json similarity index 100% rename from tools/jotpluggler/layouts/locationd_debug.json rename to openpilot/tools/jotpluggler/layouts/locationd_debug.json diff --git a/tools/jotpluggler/layouts/longitudinal.json b/openpilot/tools/jotpluggler/layouts/longitudinal.json similarity index 100% rename from tools/jotpluggler/layouts/longitudinal.json rename to openpilot/tools/jotpluggler/layouts/longitudinal.json diff --git a/tools/jotpluggler/layouts/max-torque-debug.json b/openpilot/tools/jotpluggler/layouts/max-torque-debug.json similarity index 100% rename from tools/jotpluggler/layouts/max-torque-debug.json rename to openpilot/tools/jotpluggler/layouts/max-torque-debug.json diff --git a/tools/jotpluggler/layouts/new-layout.json b/openpilot/tools/jotpluggler/layouts/new-layout.json similarity index 100% rename from tools/jotpluggler/layouts/new-layout.json rename to openpilot/tools/jotpluggler/layouts/new-layout.json diff --git a/tools/jotpluggler/layouts/system_lag_debug.json b/openpilot/tools/jotpluggler/layouts/system_lag_debug.json similarity index 100% rename from tools/jotpluggler/layouts/system_lag_debug.json rename to openpilot/tools/jotpluggler/layouts/system_lag_debug.json diff --git a/tools/jotpluggler/layouts/thermal_debug.json b/openpilot/tools/jotpluggler/layouts/thermal_debug.json similarity index 100% rename from tools/jotpluggler/layouts/thermal_debug.json rename to openpilot/tools/jotpluggler/layouts/thermal_debug.json diff --git a/tools/jotpluggler/layouts/torque-controller.json b/openpilot/tools/jotpluggler/layouts/torque-controller.json similarity index 100% rename from tools/jotpluggler/layouts/torque-controller.json rename to openpilot/tools/jotpluggler/layouts/torque-controller.json diff --git a/tools/jotpluggler/layouts/tuning.json b/openpilot/tools/jotpluggler/layouts/tuning.json similarity index 100% rename from tools/jotpluggler/layouts/tuning.json rename to openpilot/tools/jotpluggler/layouts/tuning.json diff --git a/tools/jotpluggler/layouts/ublox-debug.json b/openpilot/tools/jotpluggler/layouts/ublox-debug.json similarity index 100% rename from tools/jotpluggler/layouts/ublox-debug.json rename to openpilot/tools/jotpluggler/layouts/ublox-debug.json diff --git a/tools/jotpluggler/logs.cc b/openpilot/tools/jotpluggler/logs.cc similarity index 100% rename from tools/jotpluggler/logs.cc rename to openpilot/tools/jotpluggler/logs.cc diff --git a/tools/jotpluggler/main.cc b/openpilot/tools/jotpluggler/main.cc similarity index 100% rename from tools/jotpluggler/main.cc rename to openpilot/tools/jotpluggler/main.cc diff --git a/tools/jotpluggler/map.cc b/openpilot/tools/jotpluggler/map.cc similarity index 100% rename from tools/jotpluggler/map.cc rename to openpilot/tools/jotpluggler/map.cc diff --git a/tools/jotpluggler/map.h b/openpilot/tools/jotpluggler/map.h similarity index 100% rename from tools/jotpluggler/map.h rename to openpilot/tools/jotpluggler/map.h diff --git a/tools/jotpluggler/math_eval.py b/openpilot/tools/jotpluggler/math_eval.py similarity index 100% rename from tools/jotpluggler/math_eval.py rename to openpilot/tools/jotpluggler/math_eval.py diff --git a/tools/jotpluggler/plot.cc b/openpilot/tools/jotpluggler/plot.cc similarity index 100% rename from tools/jotpluggler/plot.cc rename to openpilot/tools/jotpluggler/plot.cc diff --git a/tools/jotpluggler/render.cc b/openpilot/tools/jotpluggler/render.cc similarity index 100% rename from tools/jotpluggler/render.cc rename to openpilot/tools/jotpluggler/render.cc diff --git a/tools/jotpluggler/runtime.cc b/openpilot/tools/jotpluggler/runtime.cc similarity index 98% rename from tools/jotpluggler/runtime.cc rename to openpilot/tools/jotpluggler/runtime.cc index 3247a5d01d..a1e47c7e8e 100644 --- a/tools/jotpluggler/runtime.cc +++ b/openpilot/tools/jotpluggler/runtime.cc @@ -1,13 +1,13 @@ #include "tools/jotpluggler/app.h" #include "tools/jotpluggler/common.h" -#include "cereal/services.h" +#include "openpilot/cereal/services.h" #include "common/timing.h" #include "imgui_impl_glfw.h" #include "imgui_impl_opengl3.h" #include "imgui_impl_opengl3_loader.h" #include "implot.h" -#include "libyuv.h" +#include "common/yuv.h" #include "msgq_repo/msgq/ipc.h" #include "tools/replay/framereader.h" @@ -1173,14 +1173,14 @@ struct CameraFeedView::Impl { result.width = reader->width; result.height = reader->height; result.rgba.resize(static_cast(result.width) * static_cast(result.height) * 4U, 0); - libyuv::NV12ToABGR(decode_buffer.y, - static_cast(decode_buffer.stride), - decode_buffer.uv, - static_cast(decode_buffer.stride), - result.rgba.data(), - result.width * 4, - result.width, - result.height); + yuv::nv12_to_rgba(decode_buffer.y, + static_cast(decode_buffer.stride), + decode_buffer.uv, + static_cast(decode_buffer.stride), + result.rgba.data(), + result.width * 4, + result.width, + result.height); result.success = true; result.decode_ms = std::chrono::duration(std::chrono::steady_clock::now() - decode_begin).count(); publish_result(*request, std::move(result)); @@ -1203,14 +1203,14 @@ struct CameraFeedView::Impl { .height = reader->height, }; prefetched.rgba.resize(static_cast(prefetched.width) * static_cast(prefetched.height) * 4U, 0); - libyuv::NV12ToABGR(decode_buffer.y, - static_cast(decode_buffer.stride), - decode_buffer.uv, - static_cast(decode_buffer.stride), - prefetched.rgba.data(), - prefetched.width * 4, - prefetched.width, - prefetched.height); + yuv::nv12_to_rgba(decode_buffer.y, + static_cast(decode_buffer.stride), + decode_buffer.uv, + static_cast(decode_buffer.stride), + prefetched.rgba.data(), + prefetched.width * 4, + prefetched.width, + prefetched.height); remember_cached_result(prefetched); } } diff --git a/tools/jotpluggler/session.cc b/openpilot/tools/jotpluggler/session.cc similarity index 99% rename from tools/jotpluggler/session.cc rename to openpilot/tools/jotpluggler/session.cc index beb0a292be..4c694a8daa 100644 --- a/tools/jotpluggler/session.cc +++ b/openpilot/tools/jotpluggler/session.cc @@ -19,6 +19,9 @@ void sync_camera_feeds(AppSession *session) { session->pane_camera_feeds[i]->setCameraIndex(session->route_data.*(kCameraViewSpecs[i].route_member), kCameraViewSpecs[i].view); } } + if (session->thumbnail_view) { + session->thumbnail_view->setThumbnails(session->route_data.thumbnails); + } } void apply_route_data(AppSession *session, UiState *state, RouteData route_data) { diff --git a/tools/jotpluggler/sidebar.cc b/openpilot/tools/jotpluggler/sidebar.cc similarity index 100% rename from tools/jotpluggler/sidebar.cc rename to openpilot/tools/jotpluggler/sidebar.cc diff --git a/tools/jotpluggler/sketch_layout.cc b/openpilot/tools/jotpluggler/sketch_layout.cc similarity index 95% rename from tools/jotpluggler/sketch_layout.cc rename to openpilot/tools/jotpluggler/sketch_layout.cc index 2091280169..1f7df72fed 100644 --- a/tools/jotpluggler/sketch_layout.cc +++ b/openpilot/tools/jotpluggler/sketch_layout.cc @@ -98,6 +98,7 @@ struct LoadedRouteArtifacts { std::vector can_messages; std::vector logs; std::vector timeline; + std::vector thumbnails; std::unordered_map enum_info; }; @@ -385,8 +386,8 @@ std::string detect_dbc_for_fingerprint(std::string_view car_fingerprint) { std::vector available_dbc_names_impl() { std::set names; for (const fs::path &dbc_dir : { - repo_root() / "opendbc" / "dbc", - repo_root() / "tools" / "jotpluggler" / "generated_dbcs", + repo_root() / "opendbc_repo" / "opendbc" / "dbc", + repo_root() / "openpilot" / "tools" / "jotpluggler" / "generated_dbcs", }) { if (fs::exists(dbc_dir) && fs::is_directory(dbc_dir)) { for (const auto &entry : fs::directory_iterator(dbc_dir)) { @@ -407,8 +408,8 @@ std::vector available_dbc_names_impl() { fs::path resolve_dbc_path(const std::string &dbc_name) { for (const fs::path &candidate : { - repo_root() / "opendbc" / "dbc" / (dbc_name + ".dbc"), - repo_root() / "tools" / "jotpluggler" / "generated_dbcs" / (dbc_name + ".dbc"), + repo_root() / "opendbc_repo" / "opendbc" / "dbc" / (dbc_name + ".dbc"), + repo_root() / "openpilot" / "tools" / "jotpluggler" / "generated_dbcs" / (dbc_name + ".dbc"), }) { if (fs::exists(candidate)) return candidate; } @@ -432,7 +433,7 @@ std::array parse_color(std::string_view color) { return out; } -uint8_t android_priority_to_level(uint8_t priority) { +uint8_t operating_system_priority_to_level(uint8_t priority) { switch (priority) { case 2: case 3: @@ -491,7 +492,7 @@ void append_timeline_entry(std::vector *timeline, double mono_tim }); } -double android_wall_time_seconds(uint64_t timestamp) { +double operating_system_wall_time_seconds(uint64_t timestamp) { if (timestamp == 0) return 0.0; if (timestamp > 1000000000000ULL) return static_cast(timestamp) / 1.0e9; if (timestamp > 1000000000ULL) return static_cast(timestamp) / 1.0e6; @@ -609,13 +610,13 @@ void append_log_event(cereal::Event::Which which, logs->push_back(std::move(entry)); break; } - case cereal::Event::Which::ANDROID_LOG: { - const auto android = event.getAndroidLog(); - auto entry = make_entry(LogOrigin::Android, android_priority_to_level(android.getPriority())); - entry.wall_time = android_wall_time_seconds(android.getTs()); - entry.source = android.hasTag() ? android.getTag().cStr() : "android"; - entry.message = android.hasMessage() ? android.getMessage().cStr() : std::string(); - entry.context = "pid=" + std::to_string(android.getPid()) + ", tid=" + std::to_string(android.getTid()); + case cereal::Event::Which::OPERATING_SYSTEM_LOG: { + const auto operating_system_log = event.getOperatingSystemLog(); + auto entry = make_entry(LogOrigin::OperatingSystem, operating_system_priority_to_level(operating_system_log.getPriority())); + entry.wall_time = operating_system_wall_time_seconds(operating_system_log.getTs()); + entry.source = operating_system_log.hasTag() ? operating_system_log.getTag().cStr() : "operating_system"; + entry.message = operating_system_log.hasMessage() ? operating_system_log.getMessage().cStr() : std::string(); + entry.context = "pid=" + std::to_string(operating_system_log.getPid()) + ", tid=" + std::to_string(operating_system_log.getTid()); if (!entry.message.empty()) { std::string err; if (const auto p = json11::Json::parse(entry.message, err); err.empty() && p.is_object()) { @@ -623,10 +624,10 @@ void append_log_event(cereal::Event::Which which, if (p["SYSLOG_IDENTIFIER"].is_string() && !p["SYSLOG_IDENTIFIER"].string_value().empty()) entry.source = p["SYSLOG_IDENTIFIER"].string_value(); if (auto pri = json_int_value(p["PRIORITY"]); pri.has_value()) - entry.level = android_priority_to_level(*pri); + entry.level = operating_system_priority_to_level(*pri); if (auto ts = json_u64_value(p["__REALTIME_TIMESTAMP"]); ts.has_value()) - entry.wall_time = android_wall_time_seconds(*ts); - entry.context = format_journal_context(p, android.getPid(), android.getTid()); + entry.wall_time = operating_system_wall_time_seconds(*ts); + entry.context = format_journal_context(p, operating_system_log.getPid(), operating_system_log.getTid()); } } logs->push_back(std::move(entry)); @@ -684,6 +685,25 @@ std::vector extract_segment_logs(const std::vector &events) { return logs; } +std::vector extract_segment_thumbnails(const std::vector &events, int segment) { + std::vector thumbnails; + for (const Event &event_record : events) { + if (event_record.which != cereal::Event::Which::THUMBNAIL) continue; + with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) { + const auto thumbnail = event.getThumbnail(); + const auto jpeg = thumbnail.getThumbnail(); + if (jpeg.size() == 0) return; + const uint64_t timestamp = thumbnail.getTimestampEof(); + ThumbnailFrame frame; + frame.timestamp = static_cast(timestamp != 0 ? timestamp : event.getLogMonoTime()) / 1.0e9; + frame.segment = segment; + frame.jpeg.assign(jpeg.begin(), jpeg.end()); + thumbnails.push_back(std::move(frame)); + }); + } + return thumbnails; +} + RouteMetadata extract_segment_metadata(const std::vector &events) { RouteMetadata metadata; for (const Event &event_record : events) { @@ -796,6 +816,8 @@ Pane parse_dock_area(const json11::Json &dock_area_node) { const std::string kind = dock_area_node["kind"].string_value(); if (kind == "map") { pane.kind = PaneKind::Map; + } else if (kind == "thumbnail") { + pane.kind = PaneKind::Thumbnail; } else if (kind == "camera") { pane.kind = PaneKind::Camera; const std::string camera_view = dock_area_node["camera_view"].string_value(); @@ -1167,6 +1189,7 @@ RouteData build_route_data(std::vector &&series_list, std::vector &&can_messages, std::vector &&logs, std::vector &&timeline, + std::vector &&thumbnails, std::unordered_map &&enum_info, std::string car_fingerprint, std::string dbc_name) { @@ -1233,6 +1256,14 @@ RouteData build_route_data(std::vector &&series_list, route_data.x_min = timeline.front().start_time; route_data.x_max = timeline.back().end_time; } + std::sort(thumbnails.begin(), thumbnails.end(), [](const ThumbnailFrame &a, const ThumbnailFrame &b) { + return a.timestamp < b.timestamp; + }); + if (!route_data.has_time_range && !thumbnails.empty()) { + route_data.has_time_range = true; + route_data.x_min = thumbnails.front().timestamp; + route_data.x_max = thumbnails.back().timestamp; + } if (route_data.has_time_range) { const double time_offset = route_data.x_min; @@ -1254,6 +1285,9 @@ RouteData build_route_data(std::vector &&series_list, entry.start_time -= time_offset; entry.end_time -= time_offset; } + for (ThumbnailFrame &thumbnail : thumbnails) { + thumbnail.timestamp -= time_offset; + } route_data.x_max -= time_offset; route_data.x_min = 0.0; } @@ -1271,6 +1305,7 @@ RouteData build_route_data(std::vector &&series_list, merged_timeline.push_back(std::move(entry)); } route_data.timeline = std::move(merged_timeline); + route_data.thumbnails = std::move(thumbnails); std::sort(can_messages.begin(), can_messages.end(), [](const CanMessageData &a, const CanMessageData &b) { return std::make_tuple(a.id.service, a.id.bus, a.id.address) < std::make_tuple(b.id.service, b.id.bus, b.id.address); @@ -1524,6 +1559,7 @@ LoadedRouteArtifacts load_route_series_parallel( SeriesAccumulator series; std::vector logs; std::vector timeline; + std::vector thumbnails; }; const std::vector> segment_list(segments.begin(), segments.end()); @@ -1586,6 +1622,7 @@ LoadedRouteArtifacts load_route_series_parallel( results[index].series = extract_segment_series(reader.events, schema, can_dbc, skip_raw_can, worker_budget, segment_workers); results[index].logs = extract_segment_logs(reader.events); results[index].timeline = extract_segment_timeline(reader.events); + results[index].thumbnails = extract_segment_thumbnails(reader.events, segment_number); segment_stats.extract_seconds = std::chrono::duration(LoadStats::Clock::now() - extract_start).count(); segment_stats.event_count = reader.events.size(); segment_stats.series_count = populated_series_count(results[index].series); @@ -1612,6 +1649,7 @@ LoadedRouteArtifacts load_route_series_parallel( } std::vector logs; std::vector timeline; + std::vector thumbnails; for (SegmentResult &result : results) { if (!result.logs.empty()) { logs.insert(logs.end(), @@ -1623,12 +1661,18 @@ LoadedRouteArtifacts load_route_series_parallel( std::make_move_iterator(result.timeline.begin()), std::make_move_iterator(result.timeline.end())); } + if (!result.thumbnails.empty()) { + thumbnails.insert(thumbnails.end(), + std::make_move_iterator(result.thumbnails.begin()), + std::make_move_iterator(result.thumbnails.end())); + } } LoadedRouteArtifacts artifacts; artifacts.series = collect_series(std::move(merged)); artifacts.can_messages = std::move(merged.can_messages); artifacts.logs = std::move(logs); artifacts.timeline = std::move(timeline); + artifacts.thumbnails = std::move(thumbnails); artifacts.enum_info = std::move(merged.enum_info); stats->merge_end = LoadStats::Clock::now(); return artifacts; @@ -1834,6 +1878,7 @@ RouteData load_route_data(const std::string &route_name, std::move(artifacts.can_messages), std::move(artifacts.logs), std::move(artifacts.timeline), + std::move(artifacts.thumbnails), std::move(artifacts.enum_info), metadata.car_fingerprint, resolved_dbc); diff --git a/tools/jotpluggler/stream.cc b/openpilot/tools/jotpluggler/stream.cc similarity index 100% rename from tools/jotpluggler/stream.cc rename to openpilot/tools/jotpluggler/stream.cc diff --git a/openpilot/tools/jotpluggler/test_jotpluggler.py b/openpilot/tools/jotpluggler/test_jotpluggler.py new file mode 100644 index 0000000000..0729e3b2e3 --- /dev/null +++ b/openpilot/tools/jotpluggler/test_jotpluggler.py @@ -0,0 +1,12 @@ +import subprocess +from pathlib import Path + + +JOTPLUGGLER_DIR = Path(__file__).parent + + +class TestJotpluggler: + def test_help(self): + result = subprocess.run(["./jotpluggler", "-h"], cwd=JOTPLUGGLER_DIR, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + assert "Usage:" in result.stderr diff --git a/openpilot/tools/jotpluggler/thumbnail.cc b/openpilot/tools/jotpluggler/thumbnail.cc new file mode 100644 index 0000000000..11e184323a --- /dev/null +++ b/openpilot/tools/jotpluggler/thumbnail.cc @@ -0,0 +1,253 @@ +#include "tools/jotpluggler/thumbnail.h" + +#include "imgui_impl_opengl3_loader.h" + +#include +#include +#include + +extern "C" { +#include +#include +} + +namespace { + +bool decode_jpeg(const std::vector &jpeg, int *width, int *height, std::vector *rgba) { + if (jpeg.empty()) return false; + + const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_MJPEG); + AVCodecContext *context = codec != nullptr ? avcodec_alloc_context3(codec) : nullptr; + AVFrame *frame = av_frame_alloc(); + AVPacket *packet = av_packet_alloc(); + if (context == nullptr || frame == nullptr || packet == nullptr) { + av_packet_free(&packet); + av_frame_free(&frame); + avcodec_free_context(&context); + return false; + } + + const bool packet_ready = jpeg.size() <= static_cast(std::numeric_limits::max()) + && av_new_packet(packet, static_cast(jpeg.size())) >= 0; + if (packet_ready) { + std::copy(jpeg.begin(), jpeg.end(), packet->data); + } + const bool decoded = packet_ready + && avcodec_open2(context, codec, nullptr) >= 0 + && avcodec_send_packet(context, packet) >= 0 + && avcodec_receive_frame(context, frame) >= 0; + if (!decoded || frame->width <= 0 || frame->height <= 0) { + av_packet_free(&packet); + av_frame_free(&frame); + avcodec_free_context(&context); + return false; + } + + int chroma_x_shift = 0; + int chroma_y_shift = 0; + switch (static_cast(frame->format)) { + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUVJ420P: + chroma_x_shift = 1; + chroma_y_shift = 1; + break; + case AV_PIX_FMT_YUV422P: + case AV_PIX_FMT_YUVJ422P: + chroma_x_shift = 1; + break; + case AV_PIX_FMT_YUV444P: + case AV_PIX_FMT_YUVJ444P: + break; + default: + av_packet_free(&packet); + av_frame_free(&frame); + avcodec_free_context(&context); + return false; + } + + *width = frame->width; + *height = frame->height; + rgba->resize(static_cast(*width) * static_cast(*height) * 4U); + const bool full_range = frame->color_range == AVCOL_RANGE_JPEG + || frame->format == AV_PIX_FMT_YUVJ420P + || frame->format == AV_PIX_FMT_YUVJ422P + || frame->format == AV_PIX_FMT_YUVJ444P; + for (int y = 0; y < *height; ++y) { + const uint8_t *y_row = frame->data[0] + y * frame->linesize[0]; + const uint8_t *u_row = frame->data[1] + (y >> chroma_y_shift) * frame->linesize[1]; + const uint8_t *v_row = frame->data[2] + (y >> chroma_y_shift) * frame->linesize[2]; + uint8_t *out = rgba->data() + static_cast(y) * static_cast(*width) * 4U; + for (int x = 0; x < *width; ++x) { + const double luma = full_range ? static_cast(y_row[x]) + : 1.164383 * (static_cast(y_row[x]) - 16.0); + const double u = static_cast(u_row[x >> chroma_x_shift]) - 128.0; + const double v = static_cast(v_row[x >> chroma_x_shift]) - 128.0; + const double red = luma + (full_range ? 1.402 : 1.596027) * v; + const double green = luma - (full_range ? 0.344136 : 0.391762) * u + - (full_range ? 0.714136 : 0.812968) * v; + const double blue = luma + (full_range ? 1.772 : 2.017232) * u; + out[x * 4 + 0] = static_cast(std::clamp(std::lround(red), 0L, 255L)); + out[x * 4 + 1] = static_cast(std::clamp(std::lround(green), 0L, 255L)); + out[x * 4 + 2] = static_cast(std::clamp(std::lround(blue), 0L, 255L)); + out[x * 4 + 3] = 255; + } + } + + av_packet_free(&packet); + av_frame_free(&frame); + avcodec_free_context(&context); + return true; +} + +std::string format_thumbnail_time(double seconds) { + const int rounded = std::max(0, static_cast(std::lround(seconds))); + const int hours = rounded / 3600; + const int minutes = (rounded % 3600) / 60; + const int secs = rounded % 60; + if (hours > 0) { + return util::string_format("%d:%02d:%02d", hours, minutes, secs); + } + return util::string_format("%02d:%02d", minutes, secs); +} + +} // namespace + +struct ThumbnailView::Impl { + ~Impl() { + destroy_texture(); + } + + void setThumbnails(const std::vector &next_thumbnails) { + destroy_texture(); + thumbnails = &next_thumbnails; + displayed_index = -1; + failed_index = -1; + } + + void update(double tracker_time) { + if (thumbnails == nullptr || thumbnails->empty()) return; + auto it = std::lower_bound(thumbnails->begin(), thumbnails->end(), tracker_time, + [](const ThumbnailFrame &frame, double time) { + return frame.timestamp < time; + }); + if (it == thumbnails->end()) { + it = std::prev(thumbnails->end()); + } else if (it != thumbnails->begin()) { + const auto previous = std::prev(it); + if (std::abs(previous->timestamp - tracker_time) <= std::abs(it->timestamp - tracker_time)) { + it = previous; + } + } + const int index = static_cast(std::distance(thumbnails->begin(), it)); + if (index == displayed_index || index == failed_index) return; + + int width = 0; + int height = 0; + std::vector rgba; + if (!decode_jpeg(it->jpeg, &width, &height, &rgba)) { + failed_index = index; + return; + } + + if (texture == 0) { + glGenTextures(1, &texture); + } + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); + glBindTexture(GL_TEXTURE_2D, 0); + texture_width = width; + texture_height = height; + displayed_index = index; + failed_index = -1; + } + + void drawSized(ImVec2 size, bool loading) const { + size.x = std::max(1.0f, size.x); + size.y = std::max(1.0f, size.y); + ImGui::InvisibleButton("##thumbnail_sized", size); + const ImVec2 pane_min = ImGui::GetItemRectMin(); + const ImVec2 pane_max = ImGui::GetItemRectMax(); + ImDrawList *draw_list = ImGui::GetWindowDrawList(); + draw_list->AddRectFilled(pane_min, pane_max, IM_COL32(24, 24, 24, 255)); + + if (texture != 0 && texture_width > 0 && texture_height > 0) { + const float scale = std::min(size.x / static_cast(texture_width), + size.y / static_cast(texture_height)); + const ImVec2 image_size(static_cast(texture_width) * scale, + static_cast(texture_height) * scale); + const ImVec2 image_min(pane_min.x + (size.x - image_size.x) * 0.5f, + pane_min.y + (size.y - image_size.y) * 0.5f); + const ImVec2 image_max(image_min.x + image_size.x, image_min.y + image_size.y); + draw_list->AddImage(static_cast(texture), image_min, image_max); + + if (thumbnails != nullptr && displayed_index >= 0 + && displayed_index < static_cast(thumbnails->size())) { + const ThumbnailFrame &frame = (*thumbnails)[static_cast(displayed_index)]; + const std::string label = util::string_format("%s · segment %d · %d/%zu", + format_thumbnail_time(frame.timestamp).c_str(), + frame.segment, + displayed_index + 1, + thumbnails->size()); + const ImVec2 text_size = ImGui::CalcTextSize(label.c_str()); + const ImVec2 label_min(image_min.x, std::max(image_min.y, image_max.y - text_size.y - 14.0f)); + draw_list->AddRectFilled(label_min, image_max, IM_COL32(0, 0, 0, 175)); + draw_list->AddText(ImVec2(label_min.x + 7.0f, label_min.y + 7.0f), IM_COL32_WHITE, label.c_str()); + } + return; + } + + const bool has_thumbnails = thumbnails != nullptr && !thumbnails->empty(); + const char *label = loading ? "loading" : (has_thumbnails ? "invalid thumbnail" : "no thumbnails"); + const ImVec2 text_size = ImGui::CalcTextSize(label); + draw_list->AddText(ImVec2(pane_min.x + (size.x - text_size.x) * 0.5f, + pane_min.y + (size.y - text_size.y) * 0.5f), + IM_COL32(187, 187, 187, 255), label); + } + + void destroy_texture() { + if (texture != 0) { + glDeleteTextures(1, &texture); + } + texture = 0; + texture_width = 0; + texture_height = 0; + } + + const std::vector *thumbnails = nullptr; + int displayed_index = -1; + int failed_index = -1; + GLuint texture = 0; + int texture_width = 0; + int texture_height = 0; +}; + +ThumbnailView::ThumbnailView() : impl_(std::make_unique()) {} +ThumbnailView::~ThumbnailView() = default; + +void ThumbnailView::setThumbnails(const std::vector &thumbnails) { + impl_->setThumbnails(thumbnails); +} + +void ThumbnailView::update(double tracker_time) { + impl_->update(tracker_time); +} + +void ThumbnailView::drawSized(ImVec2 size, bool loading) { + impl_->drawSized(size, loading); +} + +void draw_thumbnail_pane(AppSession *session, UiState *state) { + if (session->thumbnail_view == nullptr) { + ImGui::TextDisabled("Thumbnails unavailable"); + return; + } + if (state->has_tracker_time) { + session->thumbnail_view->update(state->tracker_time); + } + session->thumbnail_view->drawSized(ImGui::GetContentRegionAvail(), session->async_route_loading); +} diff --git a/openpilot/tools/jotpluggler/thumbnail.h b/openpilot/tools/jotpluggler/thumbnail.h new file mode 100644 index 0000000000..6970173a5c --- /dev/null +++ b/openpilot/tools/jotpluggler/thumbnail.h @@ -0,0 +1,5 @@ +#pragma once + +#include "tools/jotpluggler/app.h" + +void draw_thumbnail_pane(AppSession *session, UiState *state); diff --git a/tools/jotpluggler/util.cc b/openpilot/tools/jotpluggler/util.cc similarity index 100% rename from tools/jotpluggler/util.cc rename to openpilot/tools/jotpluggler/util.cc diff --git a/tools/jotpluggler/util.h b/openpilot/tools/jotpluggler/util.h similarity index 100% rename from tools/jotpluggler/util.h rename to openpilot/tools/jotpluggler/util.h diff --git a/tools/joystick/README.md b/openpilot/tools/joystick/README.md similarity index 92% rename from tools/joystick/README.md rename to openpilot/tools/joystick/README.md index eb67060ca8..3ce308927c 100644 --- a/tools/joystick/README.md +++ b/openpilot/tools/joystick/README.md @@ -14,7 +14,7 @@ The car must be off, and openpilot must be offroad before starting `joystick_con SSH into your comma device and start joystick_control with the following command: ```shell -tools/joystick/joystick_control.py --keyboard +openpilot/tools/joystick/joystick_control.py --keyboard ``` The available buttons and axes will print showing their key mappings. In general, the WASD keys control gas and brakes and steering torque in 5% increments. @@ -36,13 +36,13 @@ In order to use a joystick over the network, we need to run joystick_control loc 3. Run bridge with your laptop's IP address. This republishes the `testJoystick` packets sent from your laptop so that openpilot can receive them: ```shell # on your comma device - cereal/messaging/bridge {LAPTOP_IP} testJoystick + openpilot/cereal/messaging/bridge {LAPTOP_IP} testJoystick ``` 4. Start joystick_control on your laptop in ZMQ mode. ```shell # on your laptop export ZMQ=1 - tools/joystick/joystick_control.py + openpilot/tools/joystick/joystick_control.py ``` --- diff --git a/tools/joystick/joystick_control.py b/openpilot/tools/joystick/joystick_control.py similarity index 95% rename from tools/joystick/joystick_control.py rename to openpilot/tools/joystick/joystick_control.py index b1f14043b7..9a39d564db 100755 --- a/tools/joystick/joystick_control.py +++ b/openpilot/tools/joystick/joystick_control.py @@ -5,10 +5,10 @@ import threading import numpy as np from inputs import UnpluggedError, get_gamepad -from cereal import messaging +from openpilot.cereal import messaging from openpilot.common.params import Params from openpilot.common.realtime import Ratekeeper -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.tools.lib.kbhit import KBHit EXPO = 0.4 @@ -140,7 +140,7 @@ if __name__ == '__main__': print('- `R`: Resets axes') print('- `C`: Cancel cruise control') else: - print('Using joystick, make sure to run cereal/messaging/bridge on your device if running over the network!') + print('Using joystick, make sure to run openpilot/cereal/messaging/bridge on your device if running over the network!') print('If not running on a comma device, the mapping may need to be adjusted.') joystick = Keyboard() if args.keyboard else Joystick() diff --git a/tools/joystick/joystickd.py b/openpilot/tools/joystick/joystickd.py similarity index 95% rename from tools/joystick/joystickd.py rename to openpilot/tools/joystick/joystickd.py index 789dad5623..ac2f99f67e 100755 --- a/tools/joystick/joystickd.py +++ b/openpilot/tools/joystick/joystickd.py @@ -3,7 +3,8 @@ import math import numpy as np -from cereal import messaging, car +from openpilot.cereal import messaging +from opendbc.car.structs import car from opendbc.car.vehicle_model import VehicleModel from openpilot.common.realtime import DT_CTRL, Ratekeeper from openpilot.common.params import Params @@ -47,7 +48,7 @@ def joystickd_thread(): if CC.longActive: actuators.accel = 4.0 * float(np.clip(joystick_axes[0], -1, 1)) - actuators.longControlState = LongCtrlState.pid if sm['carState'].vEgo > CP.vEgoStopping else LongCtrlState.stopping + actuators.longControlState = LongCtrlState.pid if sm['carState'].vEgo > 0.1 else LongCtrlState.stopping CC.cruiseControl.resume = actuators.accel > 0.0 if CC.latActive: diff --git a/tools/lateral_maneuvers/.gitignore b/openpilot/tools/lateral_maneuvers/.gitignore similarity index 100% rename from tools/lateral_maneuvers/.gitignore rename to openpilot/tools/lateral_maneuvers/.gitignore diff --git a/tools/lateral_maneuvers/README.md b/openpilot/tools/lateral_maneuvers/README.md similarity index 95% rename from tools/lateral_maneuvers/README.md rename to openpilot/tools/lateral_maneuvers/README.md index 3a54bc7409..7a4c381b15 100644 --- a/tools/lateral_maneuvers/README.md +++ b/openpilot/tools/lateral_maneuvers/README.md @@ -28,7 +28,7 @@ Test your vehicle's lateral control tuning with this tool. The tool will test th 8. Gather the route ID and then run the report generator. The file will be exported to the same directory: ```sh - $ python tools/lateral_maneuvers/generate_report.py 98395b7c5b27882e/000001cc--5a73bde686 + $ python openpilot/tools/lateral_maneuvers/generate_report.py 98395b7c5b27882e/000001cc--5a73bde686 processing report for KIA_EV6 plotting maneuver: step right 20mph, runs: 3 diff --git a/tools/lateral_maneuvers/generate_report.py b/openpilot/tools/lateral_maneuvers/generate_report.py similarity index 82% rename from tools/lateral_maneuvers/generate_report.py rename to openpilot/tools/lateral_maneuvers/generate_report.py index 9a6fe1b979..e26690e2da 100755 --- a/tools/lateral_maneuvers/generate_report.py +++ b/openpilot/tools/lateral_maneuvers/generate_report.py @@ -11,11 +11,11 @@ from pathlib import Path import matplotlib.pyplot as plt from openpilot.common.utils import tabulate -from cereal import car +from opendbc.car.structs import car from openpilot.common.filter_simple import FirstOrderFilter from openpilot.selfdrive.controls.lib.latcontrol_torque import LP_FILTER_CUTOFF_HZ from openpilot.tools.lib.logreader import LogReader -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.constants import CV from openpilot.tools.longitudinal_maneuvers.generate_report import format_car_params @@ -51,6 +51,8 @@ def report(platform, route, _description, CP, ID, maneuvers): builder.append("
\n") builder.append(f"

{description}

\n") for run, msgs in enumerate(completed_runs): + last_active = max(m.logMonoTime for m in msgs if m.which() == 'lateralManeuverPlan' and m.valid) + msgs = [m for m in msgs if m.logMonoTime <= last_active] t_carControl, carControl = zip(*[(m.logMonoTime, m.carControl) for m in msgs if m.which() == 'carControl'], strict=True) t_carState, carState = zip(*[(m.logMonoTime, m.carState) for m in msgs if m.which() == 'carState'], strict=True) t_controlsState, controlsState = zip(*[(m.logMonoTime, m.controlsState) for m in msgs if m.which() == 'controlsState'], strict=True) @@ -77,7 +79,7 @@ def report(platform, route, _description, CP, ID, maneuvers): v_ego = [m.vEgo for m in carState] cross_markers = [] - if description.startswith('sine'): + if description.startswith(('sine', 'jitter')): amplitude = max(abs(lat_accel(lp.desiredCurvature, v) - baseline_accel) for lp, v in zip(lateralPlan, v_ego, strict=False)) threshold = amplitude * 0.5 @@ -128,58 +130,58 @@ def report(platform, route, _description, CP, ID, maneuvers): target_cross_times.setdefault(description, []) plt.rcParams['font.size'] = 40 - fig = plt.figure(figsize=(30, 30)) - ax = fig.subplots(4, 1, sharex=True, gridspec_kw={'height_ratios': [5, 3, 3, 3]}) + fig = plt.figure(figsize=(30, 40)) + ax = fig.subplots(5, 1, sharex=True, gridspec_kw={'height_ratios': [5, 5, 3, 3, 3]}) ax[0].grid(linewidth=4) + desired_label = 'lateralManeuverPlan.desiredCurvature * vEgo^2' desired_lat_accel = [lat_accel(m.desiredCurvature, v) for m, v in zip(lateralPlan, v_ego, strict=False)] - if description.startswith('sine'): - ax[0].plot(t_lateralPlan[:len(desired_lat_accel)], desired_lat_accel, label='desired lat accel', linewidth=6) + if description.startswith(('sine', 'jitter')): + ax[0].plot(t_lateralPlan[:len(desired_lat_accel)], desired_lat_accel, 'C1', label=desired_label, linewidth=6) else: t_desired = [t_lateralPlan[0]] + t_lateralPlan[:len(desired_lat_accel)] desired_lat_accel = [baseline_accel] + desired_lat_accel - ax[0].step(t_desired, desired_lat_accel, label='desired lat accel', linewidth=6, where='post') + ax[0].step(t_desired, desired_lat_accel, 'C1', label=desired_label, linewidth=6, where='post') actual_lat_accel = [lat_accel(cs.curvature, v) for cs, v in zip(controlsState, v_ego, strict=False)] - ax[0].plot(t_controlsState[:len(actual_lat_accel)], actual_lat_accel, label='actual lat accel', linewidth=6) + ax[0].plot(t_controlsState[:len(actual_lat_accel)], actual_lat_accel, 'g', label='controlsState.curvature * vEgo^2', linewidth=6) ax[0].set_ylabel('Lateral Accel (m/s^2)') - for ct, cv in cross_markers: ax[0].plot(ct, cv, marker='o', markersize=50, markeredgewidth=7, markeredgecolor='black', markerfacecolor='None') - - ax2 = ax[0].twinx() - if CP.steerControlType == car.CarParams.SteerControlType.angle: - ax2.plot(t_carOutput, [-m.actuatorsOutput.steeringAngleDeg for m in carOutput], 'C2', label='steer angle', linewidth=6) - else: - ax2.plot(t_carOutput, [-m.actuatorsOutput.torque for m in carOutput], 'C2', label='steer torque', linewidth=6) - - h1, l1 = ax[0].get_legend_handles_labels() - h2, l2 = ax2.get_legend_handles_labels() - ax[0].legend(h1 + h2, l1 + l2, prop={'size': 30}) + ax[0].legend(prop={'size': 30}) ax[1].grid(linewidth=4) - ax[1].plot(t_carState, [v * CV.MS_TO_MPH for v in v_ego], label='vEgo', linewidth=6) - ax[1].set_ylabel('Velocity (mph)') - ax[1].yaxis.set_major_formatter(plt.FormatStrFormatter('%.1f')) - ax[1].legend() + if CP.steerControlType == car.CarParams.SteerControlType.angle: + steer_field, steer_ylabel = 'steeringAngleDeg', 'Steer angle (deg)' + elif CP.steerControlType == car.CarParams.SteerControlType.curvature: + steer_field, steer_ylabel = 'curvature', 'Curvature (1/m)' + else: + steer_field, steer_ylabel = 'torque', 'Steer torque' + ax[1].plot(t_carControl, [getattr(m.actuators, steer_field) for m in carControl], 'C1', label=f'carControl.actuators.{steer_field}', linewidth=6) + ax[1].plot(t_carOutput, [getattr(m.actuatorsOutput, steer_field) for m in carOutput], 'g', label=f'carOutput.actuatorsOutput.{steer_field}', linewidth=6) + ax[1].set_ylabel(steer_ylabel) + ax[1].legend(prop={'size': 30}) + + ax[2].grid(linewidth=4) + ax[2].plot(t_carState, [v * CV.MS_TO_MPH for v in v_ego], label='carState.vEgo', linewidth=6) + ax[2].set_ylabel('Velocity (mph)') + ax[2].yaxis.set_major_formatter(plt.FormatStrFormatter('%.1f')) + ax[2].legend() t_accel = np.array(t_controlsState[:len(actual_lat_accel)]) raw_jerk = np.gradient(actual_lat_accel, t_accel) dt_avg = np.mean(np.diff(t_accel)) jerk_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), dt_avg) filtered_jerk = [jerk_filter.update(j) for j in raw_jerk] - ax[2].grid(linewidth=4) - ax[2].plot(t_accel, filtered_jerk, label='actual jerk', linewidth=6) - if CP.steerControlType == car.CarParams.SteerControlType.torque: - desired_jerk = [cs.lateralControlState.torqueState.desiredLateralJerk for cs in controlsState] - ax[2].plot(t_controlsState[:len(controlsState)], desired_jerk, label='desired jerk', linewidth=6) - ax[2].set_ylabel('Jerk (m/s^3)') - ax[2].legend() - ax[3].grid(linewidth=4) - ax[3].plot(t_carControl, [math.degrees(m.orientationNED[0]) for m in carControl], label='roll', linewidth=6) - ax[3].set_ylabel('Roll (deg)') + ax[3].plot(t_accel, filtered_jerk, label='d/dt(controlsState.curvature * vEgo^2)', linewidth=6) + ax[3].set_ylabel('Jerk (m/s^3)') ax[3].legend() + ax[4].grid(linewidth=4) + ax[4].plot(t_carControl, [math.degrees(m.orientationNED[0]) for m in carControl], label='carControl.orientationNED[0]', linewidth=6) + ax[4].set_ylabel('Roll (deg)') + ax[4].legend() + ax[-1].set_xlabel("Time (s)") fig.tight_layout() diff --git a/tools/lateral_maneuvers/lateral_maneuversd.py b/openpilot/tools/lateral_maneuvers/lateral_maneuversd.py similarity index 85% rename from tools/lateral_maneuvers/lateral_maneuversd.py rename to openpilot/tools/lateral_maneuvers/lateral_maneuversd.py index 4f68d9be08..ed07c896f9 100755 --- a/tools/lateral_maneuvers/lateral_maneuversd.py +++ b/openpilot/tools/lateral_maneuvers/lateral_maneuversd.py @@ -2,7 +2,8 @@ import numpy as np from dataclasses import dataclass -from cereal import messaging, car +from openpilot.cereal import messaging +from opendbc.car.structs import car from openpilot.common.constants import CV from openpilot.common.realtime import DT_MDL from openpilot.common.params import Params @@ -12,7 +13,7 @@ from openpilot.tools.longitudinal_maneuvers.maneuversd import Action, Maneuver a # thresholds for starting maneuvers MAX_SPEED_DEV = 0.7 # deviation in m/s -MAX_CURV = 0.002 # 500 m radius +MAX_CURV = 0.004 # 250 m radius MAX_ROLL = 0.12 # 6.8° TIMER = 2.0 # sec stable conditions before starting maneuver @@ -66,6 +67,12 @@ MANEUVERS = [ repeat=2, initial_speed=20. * CV.MPH_TO_MS, ), + Maneuver( + "jitter 20mph", + [Action([-0.5 if i % 2 == 0 else 0.5], [0.1]) for i in range(10)], + repeat=2, + initial_speed=20. * CV.MPH_TO_MS, + ), Maneuver( "step right 30mph", [Action([0.5], [1.0]), Action([-0.5], [1.5])], @@ -84,6 +91,12 @@ MANEUVERS = [ repeat=2, initial_speed=30. * CV.MPH_TO_MS, ), + Maneuver( + "jitter 30mph", + [Action([-0.5 if i % 2 == 0 else 0.5], [0.1]) for i in range(10)], + repeat=2, + initial_speed=30. * CV.MPH_TO_MS, + ), ] @@ -98,6 +111,8 @@ def main(): maneuvers = iter(MANEUVERS) maneuver = None complete_cnt = 0 + aborted_cnt = 0 + abort_reason = '' display_holdoff = 0 prev_text = '' @@ -121,8 +136,14 @@ def main(): alert_msg.alertDebug.alertText1 = 'Completed' alert_msg.alertDebug.alertText2 = maneuver.description elif maneuver is not None: - # reset maneuver on steering override or out of range speed - if sm['carState'].steeringPressed or (maneuver.active and abs(v_ego - maneuver.initial_speed) > MAX_SPEED_DEV): + # any driver input aborts the maneuver + CS = sm['carState'] + if CS.steeringPressed or CS.gasPressed: + aborted_cnt = int(1.0 / DT_MDL) + abort_reason = ('steering pressed' if CS.steeringPressed else 'gas pressed').ljust(20) + aborted = aborted_cnt > 0 + speed_out_of_range = maneuver.active and abs(v_ego - maneuver.initial_speed) > MAX_SPEED_DEV + if aborted or speed_out_of_range: maneuver.reset() roll = sm['carControl'].orientationNED[0] if len(sm['carControl'].orientationNED) == 3 else 0.0 @@ -140,6 +161,9 @@ def main(): else: alert_msg.alertDebug.alertText1 = f'Active {accel:+.1f}m/s² {max(action_remaining, 0):.1f}s' alert_msg.alertDebug.alertText2 = maneuver.description + elif aborted_cnt > 0: + aborted_cnt -= 1 + alert_msg.alertDebug.alertText1 = abort_reason elif not (abs(v_ego - maneuver.initial_speed) < MAX_SPEED_DEV and sm['carControl'].latActive): alert_msg.alertDebug.alertText1 = f'Set speed to {maneuver.initial_speed * CV.MS_TO_MPH:0.0f} mph' elif maneuver._ready_cnt > 0: diff --git a/tools/lib/README.md b/openpilot/tools/lib/README.md similarity index 87% rename from tools/lib/README.md rename to openpilot/tools/lib/README.md index af1ad0de22..17e24816ec 100644 --- a/tools/lib/README.md +++ b/openpilot/tools/lib/README.md @@ -1,6 +1,6 @@ ## LogReader -Route is a class for conveniently accessing all the [logs](/system/loggerd/) from your routes. The LogReader class reads the non-video logs, i.e. rlog.bz2 and qlog.bz2. There's also a matching FrameReader class for reading the videos. +Route is a class for conveniently accessing all the [logs](/openpilot/system/loggerd/) from your routes. The LogReader class reads the non-video logs, i.e. rlog.bz2 and qlog.bz2. There's also a matching FrameReader class for reading the videos. ```python from openpilot.tools.lib.route import Route diff --git a/system/ui/lib/__init__.py b/openpilot/tools/lib/__init__.py similarity index 100% rename from system/ui/lib/__init__.py rename to openpilot/tools/lib/__init__.py diff --git a/tools/lib/api.py b/openpilot/tools/lib/api.py similarity index 97% rename from tools/lib/api.py rename to openpilot/tools/lib/api.py index f84fe75869..7fca9b320f 100644 --- a/tools/lib/api.py +++ b/openpilot/tools/lib/api.py @@ -20,7 +20,7 @@ class CommaApi: resp_json = resp.json() if isinstance(resp_json, dict) and resp_json.get('error'): if resp.status_code in [401, 403]: - raise UnauthorizedError('Unauthorized. Authenticate with tools/lib/auth.py') + raise UnauthorizedError('Unauthorized. Authenticate with openpilot/tools/lib/auth.py') e = APIError(str(resp.status_code) + ":" + resp_json.get('description', str(resp_json['error']))) e.status_code = resp.status_code diff --git a/tools/lib/auth.py b/openpilot/tools/lib/auth.py similarity index 97% rename from tools/lib/auth.py rename to openpilot/tools/lib/auth.py index 5988397d0a..9139d8b42d 100755 --- a/tools/lib/auth.py +++ b/openpilot/tools/lib/auth.py @@ -54,7 +54,7 @@ class ClientRedirectHandler(BaseHTTPRequestHandler): self.end_headers() self.wfile.write(b'Return to the CLI to continue') - def log_message(self, *args): + def log_message(self, format: str, *args: object) -> None: # noqa: A002 # stdlib override pass # this prevent http server from dumping messages to stdout diff --git a/tools/lib/auth_config.py b/openpilot/tools/lib/auth_config.py similarity index 92% rename from tools/lib/auth_config.py rename to openpilot/tools/lib/auth_config.py index c4e7b6261b..5966bd34a9 100644 --- a/tools/lib/auth_config.py +++ b/openpilot/tools/lib/auth_config.py @@ -1,6 +1,6 @@ import json import os -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths class MissingAuthConfigError(Exception): diff --git a/tools/lib/azure_container.py b/openpilot/tools/lib/azure_container.py similarity index 100% rename from tools/lib/azure_container.py rename to openpilot/tools/lib/azure_container.py diff --git a/tools/lib/bootlog.py b/openpilot/tools/lib/bootlog.py similarity index 100% rename from tools/lib/bootlog.py rename to openpilot/tools/lib/bootlog.py diff --git a/tools/lib/cache.py b/openpilot/tools/lib/cache.py similarity index 100% rename from tools/lib/cache.py rename to openpilot/tools/lib/cache.py diff --git a/tools/lib/comma_car_segments.py b/openpilot/tools/lib/comma_car_segments.py similarity index 97% rename from tools/lib/comma_car_segments.py rename to openpilot/tools/lib/comma_car_segments.py index cd19356d66..b27887ed29 100644 --- a/tools/lib/comma_car_segments.py +++ b/openpilot/tools/lib/comma_car_segments.py @@ -74,7 +74,7 @@ def get_repo_url(path): response = requests.head(get_repo_raw_url(path)) - if "text/plain" in response.headers.get("content-type"): + if "text/plain" in response.headers.get("content-type", ""): # This is an LFS pointer, so download the raw data from lfs response = requests.get(get_repo_raw_url(path)) assert response.status_code == 200 diff --git a/tools/lib/exceptions.py b/openpilot/tools/lib/exceptions.py similarity index 100% rename from tools/lib/exceptions.py rename to openpilot/tools/lib/exceptions.py diff --git a/tools/lib/file_downloader.py b/openpilot/tools/lib/file_downloader.py similarity index 98% rename from tools/lib/file_downloader.py rename to openpilot/tools/lib/file_downloader.py index 5b31a5894c..68061b201e 100755 --- a/tools/lib/file_downloader.py +++ b/openpilot/tools/lib/file_downloader.py @@ -17,7 +17,7 @@ import sys import tempfile import shutil -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.tools.lib.api import CommaApi, UnauthorizedError, APIError from openpilot.tools.lib.auth_config import get_token from openpilot.tools.lib.url_file import URLFile diff --git a/tools/lib/file_sources.py b/openpilot/tools/lib/file_sources.py similarity index 92% rename from tools/lib/file_sources.py rename to openpilot/tools/lib/file_sources.py index cb7bf15114..2d2bfccfd7 100755 --- a/tools/lib/file_sources.py +++ b/openpilot/tools/lib/file_sources.py @@ -12,7 +12,7 @@ Source = Callable[[SegmentRange, list[int], FileNames], dict[int, str]] InternalUnavailableException = Exception("Internal source not available") -def comma_api_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames) -> dict[int, str]: +def comma_api_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames, /) -> dict[int, str]: route = Route(sr.route_name) # comma api will have already checked if the file exists @@ -22,7 +22,7 @@ def comma_api_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames) -> d return {seg: route.qlog_paths()[seg] for seg in seg_idxs if route.qlog_paths()[seg] is not None} -def internal_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames, endpoint_url: str = DATA_ENDPOINT) -> dict[int, str]: +def internal_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames, /, endpoint_url: str = DATA_ENDPOINT) -> dict[int, str]: if not internal_source_available(endpoint_url): raise InternalUnavailableException @@ -32,11 +32,11 @@ def internal_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames, endpo return eval_source({seg: [get_internal_url(sr, seg, fn) for fn in fns] for seg in seg_idxs}) -def openpilotci_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames) -> dict[int, str]: +def openpilotci_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames, /) -> dict[int, str]: return eval_source({seg: [get_url(sr.route_name, seg, fn) for fn in fns] for seg in seg_idxs}) -def comma_car_segments_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames) -> dict[int, str]: +def comma_car_segments_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames, /) -> dict[int, str]: return eval_source({seg: get_comma_segments_url(sr.route_name, seg) for seg in seg_idxs}) diff --git a/tools/lib/filereader.py b/openpilot/tools/lib/filereader.py similarity index 100% rename from tools/lib/filereader.py rename to openpilot/tools/lib/filereader.py diff --git a/tools/lib/framereader.py b/openpilot/tools/lib/framereader.py similarity index 100% rename from tools/lib/framereader.py rename to openpilot/tools/lib/framereader.py diff --git a/tools/lib/github_utils.py b/openpilot/tools/lib/github_utils.py similarity index 100% rename from tools/lib/github_utils.py rename to openpilot/tools/lib/github_utils.py diff --git a/tools/lib/helpers.py b/openpilot/tools/lib/helpers.py similarity index 100% rename from tools/lib/helpers.py rename to openpilot/tools/lib/helpers.py diff --git a/tools/lib/kbhit.py b/openpilot/tools/lib/kbhit.py similarity index 100% rename from tools/lib/kbhit.py rename to openpilot/tools/lib/kbhit.py diff --git a/tools/lib/live_logreader.py b/openpilot/tools/lib/live_logreader.py similarity index 87% rename from tools/lib/live_logreader.py rename to openpilot/tools/lib/live_logreader.py index edc4ac1611..20f8c3502c 100644 --- a/tools/lib/live_logreader.py +++ b/openpilot/tools/lib/live_logreader.py @@ -1,6 +1,6 @@ import os -from cereal import log as capnp_log, messaging -from cereal.services import SERVICE_LIST +from openpilot.cereal import log as capnp_log, messaging +from openpilot.cereal.services import SERVICE_LIST from openpilot.tools.lib.logreader import LogIterable, RawLogIterable diff --git a/tools/lib/log_time_series.py b/openpilot/tools/lib/log_time_series.py similarity index 100% rename from tools/lib/log_time_series.py rename to openpilot/tools/lib/log_time_series.py diff --git a/tools/lib/logreader.py b/openpilot/tools/lib/logreader.py similarity index 99% rename from tools/lib/logreader.py rename to openpilot/tools/lib/logreader.py index 9696c8524d..805e411b53 100755 --- a/tools/lib/logreader.py +++ b/openpilot/tools/lib/logreader.py @@ -15,7 +15,7 @@ import zstandard as zstd from collections.abc import Iterable, Iterator from urllib.parse import parse_qs, urlparse -from cereal import log as capnp_log +from openpilot.cereal import log as capnp_log from openpilot.common.swaglog import cloudlog from openpilot.tools.lib.filereader import FileReader from openpilot.tools.lib.file_sources import comma_api_source, internal_source, openpilotci_source, comma_car_segments_source, Source diff --git a/tools/lib/openpilotci.py b/openpilot/tools/lib/openpilotci.py similarity index 100% rename from tools/lib/openpilotci.py rename to openpilot/tools/lib/openpilotci.py diff --git a/tools/lib/openpilotcontainers.py b/openpilot/tools/lib/openpilotcontainers.py similarity index 100% rename from tools/lib/openpilotcontainers.py rename to openpilot/tools/lib/openpilotcontainers.py diff --git a/tools/lib/route.py b/openpilot/tools/lib/route.py similarity index 100% rename from tools/lib/route.py rename to openpilot/tools/lib/route.py diff --git a/tools/lib/sanitizer.py b/openpilot/tools/lib/sanitizer.py similarity index 100% rename from tools/lib/sanitizer.py rename to openpilot/tools/lib/sanitizer.py diff --git a/system/ui/sunnypilot/widgets/helpers/__init__.py b/openpilot/tools/lib/tests/__init__.py similarity index 100% rename from system/ui/sunnypilot/widgets/helpers/__init__.py rename to openpilot/tools/lib/tests/__init__.py diff --git a/tools/lib/tests/test_caching.py b/openpilot/tools/lib/tests/test_caching.py similarity index 98% rename from tools/lib/tests/test_caching.py rename to openpilot/tools/lib/tests/test_caching.py index cb14098e6d..c70d7fc81b 100644 --- a/tools/lib/tests/test_caching.py +++ b/openpilot/tools/lib/tests/test_caching.py @@ -6,7 +6,7 @@ import tempfile import pytest from openpilot.selfdrive.test.helpers import http_server_context -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.tools.lib.url_file import URLFile, prune_cache import openpilot.tools.lib.url_file as url_file_module @@ -16,7 +16,7 @@ class CachingTestRequestHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): if self.FILE_EXISTS: - self.send_response(206 if "Range" in self.headers else 200, b'1234') + self.send_response(206 if "Range" in self.headers else 200, '1234') else: self.send_response(404) self.end_headers() diff --git a/tools/lib/tests/test_comma_car_segments.py b/openpilot/tools/lib/tests/test_comma_car_segments.py similarity index 100% rename from tools/lib/tests/test_comma_car_segments.py rename to openpilot/tools/lib/tests/test_comma_car_segments.py diff --git a/tools/lib/tests/test_logreader.py b/openpilot/tools/lib/tests/test_logreader.py similarity index 99% rename from tools/lib/tests/test_logreader.py rename to openpilot/tools/lib/tests/test_logreader.py index 123f142383..a27a348d9e 100644 --- a/tools/lib/tests/test_logreader.py +++ b/openpilot/tools/lib/tests/test_logreader.py @@ -9,7 +9,7 @@ import requests from openpilot.common.parameterized import parameterized -from cereal import log as capnp_log +from openpilot.cereal import log as capnp_log from openpilot.tools.lib.logreader import LogsUnavailable, LogIterable, LogReader, parse_indirect, ReadMode from openpilot.tools.lib.file_sources import comma_api_source, InternalUnavailableException from openpilot.tools.lib.route import SegmentRange diff --git a/tools/lib/tests/test_route_library.py b/openpilot/tools/lib/tests/test_route_library.py similarity index 100% rename from tools/lib/tests/test_route_library.py rename to openpilot/tools/lib/tests/test_route_library.py diff --git a/tools/lib/url_file.py b/openpilot/tools/lib/url_file.py similarity index 98% rename from tools/lib/url_file.py rename to openpilot/tools/lib/url_file.py index de12070465..ec0a3d5815 100644 --- a/tools/lib/url_file.py +++ b/openpilot/tools/lib/url_file.py @@ -9,7 +9,7 @@ from urllib3.response import BaseHTTPResponse from urllib3.util import Timeout from openpilot.common.utils import atomic_write -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from urllib3.exceptions import MaxRetryError # Cache chunk size @@ -21,7 +21,7 @@ logging.getLogger("urllib3").setLevel(logging.WARNING) def hash_url(link: str) -> str: - return md5((link.split("?")[0]).encode('utf-8')).hexdigest() + return md5(link.split("?", maxsplit=1)[0].encode('utf-8')).hexdigest() def prune_cache(new_entry: str | None = None) -> None: diff --git a/tools/lib/vidindex.py b/openpilot/tools/lib/vidindex.py similarity index 99% rename from tools/lib/vidindex.py rename to openpilot/tools/lib/vidindex.py index 4aef6fb4d5..b200700887 100755 --- a/tools/lib/vidindex.py +++ b/openpilot/tools/lib/vidindex.py @@ -269,7 +269,7 @@ def hevc_index(hevc_file_name: str, allow_corrupt: bool=False) -> tuple[list, in raise VideoFileInvalid("first byte must be 0x00") prefix_dat = b"" - frame_types = list() + frame_types = [] i = 1 # skip past first byte 0x00 try: diff --git a/tools/longitudinal_maneuvers/.gitignore b/openpilot/tools/longitudinal_maneuvers/.gitignore similarity index 100% rename from tools/longitudinal_maneuvers/.gitignore rename to openpilot/tools/longitudinal_maneuvers/.gitignore diff --git a/tools/longitudinal_maneuvers/README.md b/openpilot/tools/longitudinal_maneuvers/README.md similarity index 95% rename from tools/longitudinal_maneuvers/README.md rename to openpilot/tools/longitudinal_maneuvers/README.md index 643af7fd82..96b989b555 100644 --- a/tools/longitudinal_maneuvers/README.md +++ b/openpilot/tools/longitudinal_maneuvers/README.md @@ -35,7 +35,7 @@ Test your vehicle's longitudinal control tuning with this tool. The tool will te 8. Gather the route ID and then run the report generator. The file will be exported to the same directory: ```sh - $ python tools/longitudinal_maneuvers/generate_report.py 57048cfce01d9625/0000010e--5b26bc3be7 'pcm accel compensation' + $ python openpilot/tools/longitudinal_maneuvers/generate_report.py 57048cfce01d9625/0000010e--5b26bc3be7 'pcm accel compensation' processing report for LEXUS_ES_TSS2 plotting maneuver: start from stop, runs: 4 diff --git a/tools/longitudinal_maneuvers/generate_report.py b/openpilot/tools/longitudinal_maneuvers/generate_report.py similarity index 99% rename from tools/longitudinal_maneuvers/generate_report.py rename to openpilot/tools/longitudinal_maneuvers/generate_report.py index 32bdb5b1c4..dbd9f6db91 100755 --- a/tools/longitudinal_maneuvers/generate_report.py +++ b/openpilot/tools/longitudinal_maneuvers/generate_report.py @@ -12,7 +12,7 @@ import matplotlib.pyplot as plt from openpilot.common.utils import tabulate from openpilot.tools.lib.logreader import LogReader -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths def format_car_params(CP): diff --git a/tools/longitudinal_maneuvers/maneuver_helpers.py b/openpilot/tools/longitudinal_maneuvers/maneuver_helpers.py similarity index 100% rename from tools/longitudinal_maneuvers/maneuver_helpers.py rename to openpilot/tools/longitudinal_maneuvers/maneuver_helpers.py diff --git a/tools/longitudinal_maneuvers/maneuversd.py b/openpilot/tools/longitudinal_maneuvers/maneuversd.py similarity index 94% rename from tools/longitudinal_maneuvers/maneuversd.py rename to openpilot/tools/longitudinal_maneuvers/maneuversd.py index 48e7384cd3..a644e1dc32 100755 --- a/tools/longitudinal_maneuvers/maneuversd.py +++ b/openpilot/tools/longitudinal_maneuvers/maneuversd.py @@ -2,11 +2,12 @@ import numpy as np from dataclasses import dataclass -from cereal import messaging, car +from openpilot.cereal import messaging from openpilot.common.constants import CV from openpilot.common.realtime import DT_MDL from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.controls.lib.drive_helpers import should_stop @dataclass @@ -58,7 +59,7 @@ class Maneuver: return float(action_accel) - def get_accel(self, v_ego: float, long_active: bool, standstill: bool, cruise_standstill: bool) -> float: + def get_accel(self, v_ego: float, long_active: bool, standstill: bool, cruise_standstill: bool, /) -> float: ready = abs(v_ego - self.initial_speed) < 0.3 and long_active and not cruise_standstill if self.initial_speed < 0.01: ready = ready and standstill @@ -138,8 +139,8 @@ MANEUVERS = [ def main(): params = Params() - cloudlog.info("joystickd is waiting for CarParams") - CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) + cloudlog.info("maneuversd is waiting for CarParams") + params.get("CarParams", block=True) sm = messaging.SubMaster(['carState', 'carControl', 'controlsState', 'selfdriveState', 'modelV2'], poll='modelV2') pm = messaging.PubMaster(['longitudinalPlan', 'longitudinalPlanSP', 'driverAssistance', 'alertDebug']) @@ -177,7 +178,7 @@ def main(): pm.send('alertDebug', alert_msg) longitudinalPlan.aTarget = accel - longitudinalPlan.shouldStop = v_ego < CP.vEgoStopping and accel < 1e-2 + longitudinalPlan.shouldStop = should_stop(v_ego, accel) longitudinalPlan.allowBrake = True longitudinalPlan.allowThrottle = True diff --git a/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py b/openpilot/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py similarity index 95% rename from tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py rename to openpilot/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py index ae3fee7355..6b711a0847 100644 --- a/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py +++ b/openpilot/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py @@ -25,7 +25,7 @@ def get_html_from_results(results, labels, AXIS): plt.close(fig) return fig_buffer.getvalue() + '
' -def generate_mpc_tuning_report(): +def generate_mpc_tuning_report(e2e=False): htmls = [] results = {} @@ -42,6 +42,7 @@ def generate_mpc_tuning_report(): cruise_values=[100, 100], prob_lead_values=[1.0, 1.0], breakpoints=[1., 11], + e2e=e2e, ) valid, results[lead_accel] = man.evaluate() labels.append(f'{lead_accel} m/s^2 lead acceleration') @@ -63,6 +64,7 @@ def generate_mpc_tuning_report(): initial_distance_lead=140., speed_lead_values=[0.0, 0.], breakpoints=[0., 30.], + e2e=e2e, ) valid, results[speed] = man.evaluate() labels.append(f'{speed} m/s approach speed') @@ -85,6 +87,7 @@ def generate_mpc_tuning_report(): 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.], + e2e=e2e, ) valid, results[oscil] = man.evaluate() labels.append(f'{oscil} m/s oscillation size') @@ -112,6 +115,7 @@ def generate_mpc_tuning_report(): initial_distance_lead=desired_follow_distance(speed, speed), speed_lead_values=lead_speeds, breakpoints=bps, + e2e=e2e, ) valid, results[oscil] = man.evaluate() labels.append(f'{oscil} m/s oscillation size') @@ -134,6 +138,7 @@ def generate_mpc_tuning_report(): initial_distance_lead=distance, speed_lead_values=[30.0], breakpoints=[0.], + e2e=e2e, ) valid, results[distance] = man.evaluate() labels.append(f'{distance} m initial distance') @@ -155,6 +160,7 @@ def generate_mpc_tuning_report(): initial_distance_lead=distance, speed_lead_values=[20.0], breakpoints=[0.], + e2e=e2e, ) valid, results[distance] = man.evaluate() labels.append(f'{distance} m initial distance') @@ -177,6 +183,7 @@ def generate_mpc_tuning_report(): initial_distance_lead=60.0, speed_lead_values=[30.0, 30.0, 0.0], breakpoints=[0., 5., 5 + stop_time], + e2e=e2e, ) valid, results[stop_time] = man.evaluate() labels.append(f'{stop_time} seconds stop time') @@ -200,6 +207,7 @@ def generate_mpc_tuning_report(): speed_lead_values=[speed, speed, speed], prob_lead_values=[0.0, 0.0, 1.0], breakpoints=[0., 5.0, 5.01], + e2e=e2e, ) valid, results[speed] = man.evaluate() labels.append(f'{speed} m/s speed') @@ -222,6 +230,7 @@ def generate_mpc_tuning_report(): speed_lead_values=[0.0, 0.0, speed], prob_lead_values=[1.0, 1.0, 1.0], breakpoints=[0., 1.0, speed/2], + e2e=e2e, ) valid, results[speed] = man.evaluate() labels.append(f'{speed} m/s speed') @@ -245,6 +254,7 @@ def generate_mpc_tuning_report(): cruise_values=[0.0, speed], prob_lead_values=[0.0, 0.0], breakpoints=[1., 1.01], + e2e=e2e, ) valid, results[speed] = man.evaluate() labels.append(f'{speed} m/s speed') @@ -268,6 +278,7 @@ def generate_mpc_tuning_report(): cruise_values=[speed, 10.0], prob_lead_values=[0.0, 0.0], breakpoints=[1., 1.01], + e2e=e2e, ) valid, results[speed] = man.evaluate() labels.append(f'{speed} m/s speed') @@ -279,12 +290,14 @@ def generate_mpc_tuning_report(): return htmls if __name__ == '__main__': - htmls = generate_mpc_tuning_report() + e2e = '--e2e' in sys.argv + file_name = 'long_mpc_tune_report.html' + for arg in sys.argv[1:]: + if not arg.startswith('-'): + file_name = arg + break - if len(sys.argv) < 2: - file_name = 'long_mpc_tune_report.html' - else: - file_name = sys.argv[1] + htmls = generate_mpc_tuning_report(e2e=e2e) with open(file_name, 'w') as f: f.write(markdown.markdown('# MPC longitudinal tuning report')) diff --git a/tools/plotjuggler/README.md b/openpilot/tools/plotjuggler/README.md similarity index 95% rename from tools/plotjuggler/README.md rename to openpilot/tools/plotjuggler/README.md index 3249971bfc..efccbdc0bd 100644 --- a/tools/plotjuggler/README.md +++ b/openpilot/tools/plotjuggler/README.md @@ -6,7 +6,7 @@ Once you've [set up the openpilot environment](../README.md), this command will download PlotJuggler and install our plugins: -`cd tools/plotjuggler && ./juggle.py --install` +`cd openpilot/tools/plotjuggler && ./juggle.py --install` ## Usage @@ -51,7 +51,7 @@ Example using segment range: Explore live data from your car! Follow these steps to stream from your comma device to your laptop: - Enable wifi tethering on your comma device -- [SSH into your device](https://github.com/commaai/openpilot/wiki/SSH) and run `cd /data/openpilot && ./cereal/messaging/bridge` +- [SSH into your device](https://github.com/commaai/openpilot/wiki/SSH) and run `cd /data/openpilot && ./openpilot/cereal/messaging/bridge` - On your laptop, connect to the device's wifi hotspot - Start PlotJuggler with `ZMQ=1 ./juggle.py --stream`, find the `Cereal Subscriber` plugin in the dropdown under Streaming, and click `Start`. diff --git a/tools/plotjuggler/juggle.py b/openpilot/tools/plotjuggler/juggle.py similarity index 83% rename from tools/plotjuggler/juggle.py rename to openpilot/tools/plotjuggler/juggle.py index a31c62c940..6ddbdbeb3e 100755 --- a/tools/plotjuggler/juggle.py +++ b/openpilot/tools/plotjuggler/juggle.py @@ -9,7 +9,6 @@ import tempfile import requests import argparse from functools import partial - from opendbc.car.fingerprints import MIGRATION from openpilot.common.basedir import BASEDIR from openpilot.common.swaglog import cloudlog @@ -34,7 +33,7 @@ def print_jotpluggler_banner(): reset = "\033[0m" if purple else "" print(f"{purple}+-------------------------------------------------------------+{reset}") print(f"{purple}|{reset} JotPluggler is the future! Try it like this: {purple}|{reset}") - print(f"{purple}|{reset} ./tools/jotpluggler/jotpluggler --demo --layout tuning {purple}|{reset}") + print(f"{purple}|{reset} ./openpilot/tools/jotpluggler/jotpluggler --demo --layout tuning {purple}|{reset}") print(f"{purple}|{reset} {purple}|{reset}") print(f"{purple}|{reset} PlotJuggler will be deleted soon. {purple}|{reset}") print(f"{purple}|{reset} Missing a feature? Open an issue or post in #dev-openpilot. {purple}|{reset}") @@ -70,7 +69,6 @@ def get_plotjuggler_version(): def start_juggler(fn=None, dbc=None, layout=None, route_or_segment_name=None, platform=None): env = os.environ.copy() - env["BASEDIR"] = BASEDIR env["PATH"] = f"{INSTALL_DIR}:{os.getenv('PATH', '')}" if dbc: if os.path.exists(dbc): @@ -86,7 +84,21 @@ def start_juggler(fn=None, dbc=None, layout=None, route_or_segment_name=None, pl extra_args += f" --window_title \"{route_or_segment_name}{f' ({platform})' if platform is not None else ''}\"" cmd = f'{PLOTJUGGLER_BIN} --buffer_size {MAX_STREAMING_BUFFER_SIZE} --plugin_folders {INSTALL_DIR}{extra_args}' - subprocess.call(cmd, shell=True, env=env, cwd=juggle_dir) + with tempfile.TemporaryDirectory() as schema_root: + tmp_cereal = os.path.join(schema_root, "cereal") + os.mkdir(tmp_cereal) + for schema in ("log.capnp", "deprecated.capnp", "custom.capnp"): + with open(os.path.join(BASEDIR, "openpilot", "cereal", schema)) as src: + contents = src.read() + contents = contents.replace('import "/include/c++.capnp"', 'import "./include/c++.capnp"') + contents = contents.replace('import "/car.capnp"', 'import "car.capnp"') + with open(os.path.join(tmp_cereal, schema), "w") as dst: + dst.write(contents) + os.symlink(os.path.join(BASEDIR, "openpilot", "cereal", "include"), os.path.join(tmp_cereal, "include"), target_is_directory=True) + os.symlink(os.path.join(BASEDIR, "opendbc_repo", "opendbc", "car", "car.capnp"), os.path.join(tmp_cereal, "car.capnp")) + os.symlink(os.path.join(BASEDIR, "opendbc_repo", "opendbc"), os.path.join(schema_root, "opendbc"), target_is_directory=True) + env["BASEDIR"] = schema_root + subprocess.call(cmd, shell=True, env=env, cwd=juggle_dir) def process(can, lr): diff --git a/tools/plotjuggler/layouts/CAN-bus-debug.xml b/openpilot/tools/plotjuggler/layouts/CAN-bus-debug.xml similarity index 100% rename from tools/plotjuggler/layouts/CAN-bus-debug.xml rename to openpilot/tools/plotjuggler/layouts/CAN-bus-debug.xml diff --git a/tools/plotjuggler/layouts/camera-timings.xml b/openpilot/tools/plotjuggler/layouts/camera-timings.xml similarity index 100% rename from tools/plotjuggler/layouts/camera-timings.xml rename to openpilot/tools/plotjuggler/layouts/camera-timings.xml diff --git a/tools/plotjuggler/layouts/can-states.xml b/openpilot/tools/plotjuggler/layouts/can-states.xml similarity index 100% rename from tools/plotjuggler/layouts/can-states.xml rename to openpilot/tools/plotjuggler/layouts/can-states.xml diff --git a/tools/plotjuggler/layouts/controls_mismatch_debug.xml b/openpilot/tools/plotjuggler/layouts/controls_mismatch_debug.xml similarity index 100% rename from tools/plotjuggler/layouts/controls_mismatch_debug.xml rename to openpilot/tools/plotjuggler/layouts/controls_mismatch_debug.xml diff --git a/tools/plotjuggler/layouts/gps.xml b/openpilot/tools/plotjuggler/layouts/gps.xml similarity index 100% rename from tools/plotjuggler/layouts/gps.xml rename to openpilot/tools/plotjuggler/layouts/gps.xml diff --git a/tools/plotjuggler/layouts/gps_vs_llk.xml b/openpilot/tools/plotjuggler/layouts/gps_vs_llk.xml similarity index 100% rename from tools/plotjuggler/layouts/gps_vs_llk.xml rename to openpilot/tools/plotjuggler/layouts/gps_vs_llk.xml diff --git a/tools/plotjuggler/layouts/locationd_debug.xml b/openpilot/tools/plotjuggler/layouts/locationd_debug.xml similarity index 100% rename from tools/plotjuggler/layouts/locationd_debug.xml rename to openpilot/tools/plotjuggler/layouts/locationd_debug.xml diff --git a/tools/plotjuggler/layouts/longitudinal.xml b/openpilot/tools/plotjuggler/layouts/longitudinal.xml similarity index 100% rename from tools/plotjuggler/layouts/longitudinal.xml rename to openpilot/tools/plotjuggler/layouts/longitudinal.xml diff --git a/tools/plotjuggler/layouts/max-torque-debug.xml b/openpilot/tools/plotjuggler/layouts/max-torque-debug.xml similarity index 100% rename from tools/plotjuggler/layouts/max-torque-debug.xml rename to openpilot/tools/plotjuggler/layouts/max-torque-debug.xml diff --git a/tools/plotjuggler/layouts/system_lag_debug.xml b/openpilot/tools/plotjuggler/layouts/system_lag_debug.xml similarity index 100% rename from tools/plotjuggler/layouts/system_lag_debug.xml rename to openpilot/tools/plotjuggler/layouts/system_lag_debug.xml diff --git a/tools/plotjuggler/layouts/thermal_debug.xml b/openpilot/tools/plotjuggler/layouts/thermal_debug.xml similarity index 100% rename from tools/plotjuggler/layouts/thermal_debug.xml rename to openpilot/tools/plotjuggler/layouts/thermal_debug.xml diff --git a/tools/plotjuggler/layouts/torque-controller.xml b/openpilot/tools/plotjuggler/layouts/torque-controller.xml similarity index 100% rename from tools/plotjuggler/layouts/torque-controller.xml rename to openpilot/tools/plotjuggler/layouts/torque-controller.xml diff --git a/tools/plotjuggler/layouts/tuning.xml b/openpilot/tools/plotjuggler/layouts/tuning.xml similarity index 100% rename from tools/plotjuggler/layouts/tuning.xml rename to openpilot/tools/plotjuggler/layouts/tuning.xml diff --git a/tools/plotjuggler/layouts/ublox-debug.xml b/openpilot/tools/plotjuggler/layouts/ublox-debug.xml similarity index 100% rename from tools/plotjuggler/layouts/ublox-debug.xml rename to openpilot/tools/plotjuggler/layouts/ublox-debug.xml diff --git a/tools/plotjuggler/test_plotjuggler.py b/openpilot/tools/plotjuggler/test_plotjuggler.py similarity index 96% rename from tools/plotjuggler/test_plotjuggler.py rename to openpilot/tools/plotjuggler/test_plotjuggler.py index 26bad25c3e..d55aafe9be 100644 --- a/tools/plotjuggler/test_plotjuggler.py +++ b/openpilot/tools/plotjuggler/test_plotjuggler.py @@ -11,7 +11,7 @@ from openpilot.common.basedir import BASEDIR from openpilot.common.timeout import Timeout from openpilot.tools.plotjuggler.juggle import DEMO_ROUTE, install -PJ_DIR = os.path.join(BASEDIR, "tools/plotjuggler") +PJ_DIR = os.path.join(BASEDIR, "openpilot/tools/plotjuggler") class TestPlotJuggler: diff --git a/tools/replay/.gitignore b/openpilot/tools/replay/.gitignore similarity index 100% rename from tools/replay/.gitignore rename to openpilot/tools/replay/.gitignore diff --git a/tools/replay/README.md b/openpilot/tools/replay/README.md similarity index 84% rename from tools/replay/README.md rename to openpilot/tools/replay/README.md index d2beda9940..108df08935 100644 --- a/tools/replay/README.md +++ b/openpilot/tools/replay/README.md @@ -8,7 +8,7 @@ Before starting a replay, you need to authenticate with your comma account using ```bash # Authenticate to access routes from your comma account: -python3 tools/lib/auth.py +python3 openpilot/tools/lib/auth.py ``` ## Replay a Remote Route @@ -16,13 +16,13 @@ You can replay a route from your comma account by specifying the route name. ```bash # Start a replay with a specific route: -tools/replay/replay +openpilot/tools/replay/replay # Example: -tools/replay/replay '5beb9b58bd12b691/0000010a--a51155e496' +openpilot/tools/replay/replay '5beb9b58bd12b691/0000010a--a51155e496' # Replay the default demo route: -tools/replay/replay --demo +openpilot/tools/replay/replay --demo ``` ## Replay a Local Route @@ -30,14 +30,14 @@ To replay a route stored locally on your machine, specify the route name and pro ```bash # Replay a local route -tools/replay/replay --data_dir="/path_to/route" +openpilot/tools/replay/replay --data_dir="/path_to/route" # Example: # If you have a local route stored at /path_to_routes with segments like: # 5beb9b58bd12b691/0000010a--a51155e496--0 # 5beb9b58bd12b691/0000010a--a51155e496--1 # You can replay it like this: -tools/replay/replay "5beb9b58bd12b691/0000010a--a51155e496" --data_dir="/path_to_routes" +openpilot/tools/replay/replay "5beb9b58bd12b691/0000010a--a51155e496" --data_dir="/path_to_routes" ``` ## Send Messages via ZMQ @@ -45,15 +45,15 @@ By default, replay sends messages via MSGQ. To switch to ZMQ, set the ZMQ enviro ```bash # Start replay and send messages via ZMQ: -ZMQ=1 tools/replay/replay +ZMQ=1 openpilot/tools/replay/replay ``` ## Usage For more information on available options and arguments, use the help command: ``` bash -$ tools/replay/replay -h -Usage: tools/replay/replay [options] route +$ openpilot/tools/replay/replay -h +Usage: openpilot/tools/replay/replay [options] route Mock openpilot components by publishing logged messages. Options: @@ -87,16 +87,16 @@ Arguments: To visualize the replay within the openpilot UI, run the following commands: ```bash -tools/replay/replay -cd selfdrive/ui && ./ui.py +openpilot/tools/replay/replay +cd openpilot/selfdrive/ui && ./ui.py ``` ## Work with plotjuggler If you want to use replay with plotjuggler, you can stream messages by running: ```bash -tools/replay/replay -tools/plotjuggler/juggle.py --stream +openpilot/tools/replay/replay +openpilot/tools/plotjuggler/juggle.py --stream ``` ## watch3 @@ -107,10 +107,10 @@ simply replay a route using the `--dcam` and `--ecam` flags: ```bash # start a replay -cd tools/replay && ./replay --demo --dcam --ecam +cd openpilot/tools/replay && ./replay --demo --dcam --ecam # then start watch3 -cd selfdrive/ui && ./watch3.py +cd openpilot/selfdrive/ui && ./watch3.py ``` ![](https://i.imgur.com/IeaOdAb.png) diff --git a/tools/replay/SConscript b/openpilot/tools/replay/SConscript similarity index 82% rename from tools/replay/SConscript rename to openpilot/tools/replay/SConscript index d047415f58..643de97bb9 100644 --- a/tools/replay/SConscript +++ b/openpilot/tools/replay/SConscript @@ -1,4 +1,4 @@ -Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal') +Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'ffmpeg_libs') replay_env = env.Clone() replay_env['CCFLAGS'] += ['-Wno-deprecated-declarations'] @@ -12,9 +12,7 @@ if arch != "Darwin": replay_lib_src.append("qcom_decoder.cc") replay_lib = replay_env.Library("replay", replay_lib_src, LIBS=base_libs, FRAMEWORKS=base_frameworks) Export('replay_lib') -replay_libs = [replay_lib, 'avformat', 'avcodec', 'swresample', 'avutil', 'x264', 'z', 'bz2', 'zstd', 'yuv', 'ncurses'] + base_libs -if arch != "Darwin": - replay_libs += ['va', 'va-drm', 'drm'] +replay_libs = [replay_lib] + ffmpeg_libs + ['bz2', 'zstd', 'ncurses'] + base_libs replay_env.Program("replay", ["main.cc"], LIBS=replay_libs, FRAMEWORKS=base_frameworks) if GetOption('extras'): diff --git a/system/webrtc/__init__.py b/openpilot/tools/replay/__init__.py similarity index 100% rename from system/webrtc/__init__.py rename to openpilot/tools/replay/__init__.py diff --git a/tools/replay/camera.cc b/openpilot/tools/replay/camera.cc similarity index 100% rename from tools/replay/camera.cc rename to openpilot/tools/replay/camera.cc diff --git a/tools/replay/camera.h b/openpilot/tools/replay/camera.h similarity index 100% rename from tools/replay/camera.h rename to openpilot/tools/replay/camera.h diff --git a/tools/replay/can_replay.py b/openpilot/tools/replay/can_replay.py similarity index 100% rename from tools/replay/can_replay.py rename to openpilot/tools/replay/can_replay.py diff --git a/tools/replay/consoleui.cc b/openpilot/tools/replay/consoleui.cc similarity index 100% rename from tools/replay/consoleui.cc rename to openpilot/tools/replay/consoleui.cc diff --git a/tools/replay/consoleui.h b/openpilot/tools/replay/consoleui.h similarity index 100% rename from tools/replay/consoleui.h rename to openpilot/tools/replay/consoleui.h diff --git a/tools/replay/filereader.cc b/openpilot/tools/replay/filereader.cc similarity index 100% rename from tools/replay/filereader.cc rename to openpilot/tools/replay/filereader.cc diff --git a/tools/replay/filereader.h b/openpilot/tools/replay/filereader.h similarity index 100% rename from tools/replay/filereader.h rename to openpilot/tools/replay/filereader.h diff --git a/tools/replay/framereader.cc b/openpilot/tools/replay/framereader.cc similarity index 96% rename from tools/replay/framereader.cc rename to openpilot/tools/replay/framereader.cc index a643f0d3a7..7c6f144148 100644 --- a/tools/replay/framereader.cc +++ b/openpilot/tools/replay/framereader.cc @@ -6,10 +6,10 @@ #include #include "common/util.h" -#include "libyuv.h" +#include "common/yuv.h" #include "tools/replay/py_downloader.h" #include "tools/replay/util.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #ifdef __APPLE__ #define HW_DEVICE_TYPE AV_HWDEVICE_TYPE_VIDEOTOOLBOX @@ -258,12 +258,12 @@ bool FFmpegVideoDecoder::copyBuffer(AVFrame *f, VisionBuf *buf) { memcpy(buf->uv + i*buf->stride, f->data[1] + i*f->linesize[1], width); } } else { - libyuv::I420ToNV12(f->data[0], f->linesize[0], - f->data[1], f->linesize[1], - f->data[2], f->linesize[2], - buf->y, buf->stride, - buf->uv, buf->stride, - width, height); + yuv::i420_to_nv12(f->data[0], f->linesize[0], + f->data[1], f->linesize[1], + f->data[2], f->linesize[2], + buf->y, buf->stride, + buf->uv, buf->stride, + width, height); } return true; } diff --git a/tools/replay/framereader.h b/openpilot/tools/replay/framereader.h similarity index 100% rename from tools/replay/framereader.h rename to openpilot/tools/replay/framereader.h diff --git a/tools/__init__.py b/openpilot/tools/replay/lib/__init__.py similarity index 100% rename from tools/__init__.py rename to openpilot/tools/replay/lib/__init__.py diff --git a/tools/replay/lib/ui_helpers.py b/openpilot/tools/replay/lib/ui_helpers.py similarity index 96% rename from tools/replay/lib/ui_helpers.py rename to openpilot/tools/replay/lib/ui_helpers.py index 039dd4f235..b9da0d225f 100644 --- a/tools/replay/lib/ui_helpers.py +++ b/openpilot/tools/replay/lib/ui_helpers.py @@ -5,6 +5,7 @@ import numpy as np import pyray as rl from matplotlib.backends.backend_agg import FigureCanvasAgg +from matplotlib.artist import Artist from matplotlib.offsetbox import AnchoredOffsetbox, HPacker, TextArea from openpilot.common.transformations.camera import get_view_frame_from_calib_frame @@ -119,11 +120,11 @@ def init_plots(arr, name_to_arr_idx, plot_xlims, plot_ylims, plot_names, plot_co idxs.append(name_to_arr_idx[item]) plot_select.append(i) # Build colored title: each label colored to match its plot line - title_texts = [] + title_texts: list[Artist] = [] for j2, (nm, cl) in enumerate(zip(pl_list, plot_colors[i], strict=False)): if j2 > 0: - title_texts.append(TextArea(", ", textprops=dict(color="white", fontsize=10))) - title_texts.append(TextArea(nm, textprops=dict(color=label_palette[cl], fontsize=10))) + title_texts.append(TextArea(", ", textprops={"color": "white", "fontsize": 10})) + title_texts.append(TextArea(nm, textprops={"color": label_palette[cl], "fontsize": 10})) packed = HPacker(children=title_texts, pad=0, sep=0) ab = AnchoredOffsetbox(loc='lower center', child=packed, bbox_to_anchor=(0.5, 1.0), bbox_transform=axs[i].transAxes, frameon=False, pad=0) @@ -187,7 +188,7 @@ def plot_model(m, img, calibration, top_down): def plot_lead(rs, top_down): for lead in [rs.leadOne, rs.leadTwo]: - if not lead.status: + if not lead.present: continue x = lead.dRel @@ -201,7 +202,7 @@ def maybe_update_radar_points(lt, lid_overlay): if lt is not None: ar_pts = {} for track in lt: - ar_pts[track.trackId] = [track.dRel, track.yRel, track.vRel, track.aRel] + ar_pts[track.trackId] = [track.dRel, track.yRel, track.vRel] for pt in ar_pts.values(): # negative here since radar is left positive px, py = to_topdown_pt(pt[0], -pt[1]) diff --git a/tools/replay/logreader.cc b/openpilot/tools/replay/logreader.cc similarity index 100% rename from tools/replay/logreader.cc rename to openpilot/tools/replay/logreader.cc diff --git a/tools/replay/logreader.h b/openpilot/tools/replay/logreader.h similarity index 97% rename from tools/replay/logreader.h rename to openpilot/tools/replay/logreader.h index fe11ab0f77..63f7468401 100644 --- a/tools/replay/logreader.h +++ b/openpilot/tools/replay/logreader.h @@ -5,7 +5,7 @@ #include #include -#include "cereal/gen/cpp/log.capnp.h" +#include "openpilot/cereal/gen/cpp/log.capnp.h" #include "tools/replay/util.h" const CameraType ALL_CAMERAS[] = {RoadCam, DriverCam, WideRoadCam}; diff --git a/tools/replay/main.cc b/openpilot/tools/replay/main.cc similarity index 100% rename from tools/replay/main.cc rename to openpilot/tools/replay/main.cc diff --git a/tools/replay/py_downloader.cc b/openpilot/tools/replay/py_downloader.cc similarity index 100% rename from tools/replay/py_downloader.cc rename to openpilot/tools/replay/py_downloader.cc diff --git a/tools/replay/py_downloader.h b/openpilot/tools/replay/py_downloader.h similarity index 100% rename from tools/replay/py_downloader.h rename to openpilot/tools/replay/py_downloader.h diff --git a/tools/replay/qcom_decoder.cc b/openpilot/tools/replay/qcom_decoder.cc similarity index 100% rename from tools/replay/qcom_decoder.cc rename to openpilot/tools/replay/qcom_decoder.cc diff --git a/tools/replay/qcom_decoder.h b/openpilot/tools/replay/qcom_decoder.h similarity index 100% rename from tools/replay/qcom_decoder.h rename to openpilot/tools/replay/qcom_decoder.h diff --git a/tools/replay/replay.cc b/openpilot/tools/replay/replay.cc similarity index 99% rename from tools/replay/replay.cc rename to openpilot/tools/replay/replay.cc index d8d59e41a4..75aecfd0c2 100644 --- a/tools/replay/replay.cc +++ b/openpilot/tools/replay/replay.cc @@ -4,7 +4,7 @@ #include #include #include -#include "cereal/services.h" +#include "openpilot/cereal/services.h" #include "common/params.h" #include "tools/replay/util.h" @@ -193,7 +193,7 @@ void Replay::startStream(const std::shared_ptr segment) { auto event = reader.getRoot(); uint64_t wall_time = event.getInitData().getWallTimeNanos(); if (wall_time > 0) { - route_date_time_ = wall_time / 1e6; + route_date_time_ = wall_time / 1e9; } } diff --git a/tools/replay/replay.h b/openpilot/tools/replay/replay.h similarity index 99% rename from tools/replay/replay.h rename to openpilot/tools/replay/replay.h index 3e2bc7c00e..ce6d75bd6d 100644 --- a/tools/replay/replay.h +++ b/openpilot/tools/replay/replay.h @@ -100,7 +100,7 @@ private: std::atomic exit_ = false; std::atomic interrupt_requested_ = false; bool events_ready_ = false; - std::time_t route_date_time_; + std::time_t route_date_time_ = 0; uint64_t route_start_ts_ = 0; std::atomic cur_mono_time_ = 0; cereal::Event::Which cur_which_ = cereal::Event::Which::INIT_DATA; diff --git a/tools/replay/route.cc b/openpilot/tools/replay/route.cc similarity index 98% rename from tools/replay/route.cc rename to openpilot/tools/replay/route.cc index df4fa813cf..326d28d726 100644 --- a/tools/replay/route.cc +++ b/openpilot/tools/replay/route.cc @@ -5,7 +5,7 @@ #include #include "json11/json11.hpp" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include "tools/replay/py_downloader.h" #include "tools/replay/replay.h" #include "tools/replay/util.h" @@ -123,7 +123,7 @@ bool Route::loadFromServer() { if (json.is_object() && json["error"].is_string()) { const std::string &error = json["error"].string_value(); if (error == "unauthorized") { - rWarning(">> Unauthorized. Authenticate with tools/lib/auth.py <<"); + rWarning(">> Unauthorized. Authenticate with openpilot/tools/lib/auth.py <<"); err_ = RouteLoadError::Unauthorized; } else if (error == "not_found") { rWarning("The specified route could not be found on the server."); diff --git a/tools/replay/route.h b/openpilot/tools/replay/route.h similarity index 100% rename from tools/replay/route.h rename to openpilot/tools/replay/route.h diff --git a/tools/replay/seg_mgr.cc b/openpilot/tools/replay/seg_mgr.cc similarity index 100% rename from tools/replay/seg_mgr.cc rename to openpilot/tools/replay/seg_mgr.cc diff --git a/tools/replay/seg_mgr.h b/openpilot/tools/replay/seg_mgr.h similarity index 100% rename from tools/replay/seg_mgr.h rename to openpilot/tools/replay/seg_mgr.h diff --git a/tools/replay/tests/test_replay.cc b/openpilot/tools/replay/tests/test_replay.cc similarity index 100% rename from tools/replay/tests/test_replay.cc rename to openpilot/tools/replay/tests/test_replay.cc diff --git a/tools/replay/timeline.cc b/openpilot/tools/replay/timeline.cc similarity index 98% rename from tools/replay/timeline.cc rename to openpilot/tools/replay/timeline.cc index 08dca5c89e..34729f5214 100644 --- a/tools/replay/timeline.cc +++ b/openpilot/tools/replay/timeline.cc @@ -3,7 +3,7 @@ #include #include -#include "cereal/gen/cpp/log.capnp.h" +#include "openpilot/cereal/gen/cpp/log.capnp.h" Timeline::~Timeline() { should_exit_.store(true); diff --git a/tools/replay/timeline.h b/openpilot/tools/replay/timeline.h similarity index 100% rename from tools/replay/timeline.h rename to openpilot/tools/replay/timeline.h diff --git a/tools/replay/ui.py b/openpilot/tools/replay/ui.py similarity index 97% rename from tools/replay/ui.py rename to openpilot/tools/replay/ui.py index 405c79f425..4fbede2be9 100755 --- a/tools/replay/ui.py +++ b/openpilot/tools/replay/ui.py @@ -6,7 +6,7 @@ import sys import numpy as np import pyray as rl -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.common.basedir import BASEDIR from openpilot.common.transformations.camera import DEVICE_CAMERAS from openpilot.tools.replay.lib.ui_helpers import ( @@ -54,7 +54,7 @@ def ui_thread(addr): rl.set_target_fps(60) # Load font - font_path = os.path.join(BASEDIR, "selfdrive/assets/fonts/JetBrainsMono-Medium.ttf") + font_path = os.path.join(BASEDIR, "openpilot/selfdrive/assets/fonts/JetBrainsMono-Medium.ttf") font = rl.load_font_ex(font_path, 32, None, 0) camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) @@ -173,10 +173,10 @@ def ui_thread(addr): plot_arr[-1, name_to_arr_idx['angle_steers']] = sm['carState'].steeringAngleDeg plot_arr[-1, name_to_arr_idx['angle_steers_des']] = sm['carControl'].actuators.steeringAngleDeg plot_arr[-1, name_to_arr_idx['angle_steers_k']] = angle_steers_k - plot_arr[-1, name_to_arr_idx['gas']] = sm['carState'].gasDEPRECATED + plot_arr[-1, name_to_arr_idx['gas']] = sm['carState'].deprecated.gas # TODO gas is deprecated plot_arr[-1, name_to_arr_idx['computer_gas']] = np.clip(sm['carControl'].actuators.accel / 4.0, 0.0, 1.0) - plot_arr[-1, name_to_arr_idx['user_brake']] = sm['carState'].brake + plot_arr[-1, name_to_arr_idx['user_brake']] = sm['carState'].brakePressed plot_arr[-1, name_to_arr_idx['steer_torque']] = sm['carControl'].actuators.torque * ANGLE_SCALE # TODO brake is deprecated plot_arr[-1, name_to_arr_idx['computer_brake']] = np.clip(-sm['carControl'].actuators.accel / 4.0, 0.0, 1.0) diff --git a/tools/replay/util.cc b/openpilot/tools/replay/util.cc similarity index 100% rename from tools/replay/util.cc rename to openpilot/tools/replay/util.cc diff --git a/tools/replay/util.h b/openpilot/tools/replay/util.h similarity index 97% rename from tools/replay/util.h rename to openpilot/tools/replay/util.h index a2d0f6203a..03a15787b2 100644 --- a/tools/replay/util.h +++ b/openpilot/tools/replay/util.h @@ -7,7 +7,7 @@ #include #include #include -#include "cereal/messaging/messaging.h" +#include "openpilot/cereal/messaging/messaging.h" enum CameraType { RoadCam = 0, diff --git a/tools/sim/README.md b/openpilot/tools/sim/README.md similarity index 91% rename from tools/sim/README.md rename to openpilot/tools/sim/README.md index f6ddde2580..b3e60a119b 100644 --- a/tools/sim/README.md +++ b/openpilot/tools/sim/README.md @@ -7,7 +7,7 @@ openpilot implements a [bridge](run_bridge.py) that allows it to run in the [Met First, start openpilot. ``` bash # Run locally -./tools/sim/launch_openpilot.sh +./openpilot/tools/sim/launch_openpilot.sh ``` ## Bridge usage @@ -44,7 +44,7 @@ options: ## MetaDrive ### Launching Metadrive -Start bridge processes located in tools/sim: +Start bridge processes located in openpilot/tools/sim: ``` bash ./run_bridge.py ``` \ No newline at end of file diff --git a/tools/lib/__init__.py b/openpilot/tools/sim/__init__.py similarity index 100% rename from tools/lib/__init__.py rename to openpilot/tools/sim/__init__.py diff --git a/tools/lib/tests/__init__.py b/openpilot/tools/sim/bridge/__init__.py similarity index 100% rename from tools/lib/tests/__init__.py rename to openpilot/tools/sim/bridge/__init__.py diff --git a/tools/sim/bridge/common.py b/openpilot/tools/sim/bridge/common.py similarity index 99% rename from tools/sim/bridge/common.py rename to openpilot/tools/sim/bridge/common.py index 499420ec4b..1d00e3b0f9 100644 --- a/tools/sim/bridge/common.py +++ b/openpilot/tools/sim/bridge/common.py @@ -7,7 +7,6 @@ from collections import namedtuple from enum import Enum from multiprocessing import Process, Queue, Value from abc import ABC, abstractmethod - from opendbc.car.honda.values import CruiseButtons from openpilot.common.params import Params from openpilot.common.realtime import Ratekeeper @@ -95,7 +94,7 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga """) @abstractmethod - def spawn_world(self, q: Queue) -> World: + def spawn_world(self, q: Queue, /) -> World: pass def _run(self, q: Queue): diff --git a/tools/sim/bridge/metadrive/metadrive_bridge.py b/openpilot/tools/sim/bridge/metadrive/metadrive_bridge.py similarity index 66% rename from tools/sim/bridge/metadrive/metadrive_bridge.py rename to openpilot/tools/sim/bridge/metadrive/metadrive_bridge.py index b8dc94cf86..4222ba45a7 100644 --- a/tools/sim/bridge/metadrive/metadrive_bridge.py +++ b/openpilot/tools/sim/bridge/metadrive/metadrive_bridge.py @@ -29,11 +29,11 @@ def curve_block(length, angle=45, direction=0): def create_map(track_size=60): curve_len = track_size * 2 - return dict( - type=MapGenerateMethod.PG_MAP_FILE, - lane_num=2, - lane_width=4.5, - config=[ + return { + "type": MapGenerateMethod.PG_MAP_FILE, + "lane_num": 2, + "lane_width": 4.5, + "config": [ None, straight_block(track_size), curve_block(curve_len, 90), @@ -44,7 +44,7 @@ def create_map(track_size=60): straight_block(track_size), curve_block(curve_len, 90), ] - ) + } class MetaDriveBridge(SimulatorBridge): @@ -65,29 +65,29 @@ class MetaDriveBridge(SimulatorBridge): if self.dual_camera: sensors["rgb_wide"] = (RGBCameraWide, W, H) - config = dict( - use_render=self.should_render, - vehicle_config=dict( - enable_reverse=False, - render_vehicle=False, - image_source="rgb_road", - ), - sensors=sensors, - image_on_cuda=_cuda_enable, - image_observation=True, - interface_panel=[], - out_of_route_done=False, - on_continuous_line_done=False, - crash_vehicle_done=False, - crash_object_done=False, - arrive_dest_done=False, - traffic_density=0.0, # traffic is incredibly expensive - map_config=create_map(), - decision_repeat=1, - physics_world_step_size=self.TICKS_PER_FRAME/100, - preload_models=False, - show_logo=False, - anisotropic_filtering=False - ) + config = { + "use_render": self.should_render, + "vehicle_config": { + "enable_reverse": False, + "render_vehicle": False, + "image_source": "rgb_road", + }, + "sensors": sensors, + "image_on_cuda": _cuda_enable, + "image_observation": True, + "interface_panel": [], + "out_of_route_done": False, + "on_continuous_line_done": False, + "crash_vehicle_done": False, + "crash_object_done": False, + "arrive_dest_done": False, + "traffic_density": 0.0, # traffic is incredibly expensive + "map_config": create_map(), + "decision_repeat": 1, + "physics_world_step_size": self.TICKS_PER_FRAME/100, + "preload_models": False, + "show_logo": False, + "anisotropic_filtering": False + } return MetaDriveWorld(queue, config, self.test_duration, self.test_run, self.dual_camera) diff --git a/tools/sim/bridge/metadrive/metadrive_common.py b/openpilot/tools/sim/bridge/metadrive/metadrive_common.py similarity index 100% rename from tools/sim/bridge/metadrive/metadrive_common.py rename to openpilot/tools/sim/bridge/metadrive/metadrive_common.py diff --git a/tools/sim/bridge/metadrive/metadrive_process.py b/openpilot/tools/sim/bridge/metadrive/metadrive_process.py similarity index 100% rename from tools/sim/bridge/metadrive/metadrive_process.py rename to openpilot/tools/sim/bridge/metadrive/metadrive_process.py diff --git a/tools/sim/bridge/metadrive/metadrive_world.py b/openpilot/tools/sim/bridge/metadrive/metadrive_world.py similarity index 97% rename from tools/sim/bridge/metadrive/metadrive_world.py rename to openpilot/tools/sim/bridge/metadrive/metadrive_world.py index c5111289d0..54b461a46c 100644 --- a/tools/sim/bridge/metadrive/metadrive_world.py +++ b/openpilot/tools/sim/bridge/metadrive/metadrive_world.py @@ -97,7 +97,7 @@ class MetaDriveWorld(World): self.op_engaged.set() # check moving 5 seconds after engaged, doesn't move right away - after_engaged_check = is_engaged and time.monotonic() - self.first_engage >= 5 and self.test_run + after_engaged_check = is_engaged and self.first_engage is not None and time.monotonic() - self.first_engage >= 5 and self.test_run x_dist = abs(curr_pos[0] - self.vehicle_last_pos[0]) y_dist = abs(curr_pos[1] - self.vehicle_last_pos[1]) diff --git a/tools/sim/launch_openpilot.sh b/openpilot/tools/sim/launch_openpilot.sh similarity index 100% rename from tools/sim/launch_openpilot.sh rename to openpilot/tools/sim/launch_openpilot.sh diff --git a/tools/replay/__init__.py b/openpilot/tools/sim/lib/__init__.py similarity index 100% rename from tools/replay/__init__.py rename to openpilot/tools/sim/lib/__init__.py diff --git a/tools/sim/lib/camerad.py b/openpilot/tools/sim/lib/camerad.py similarity index 98% rename from tools/sim/lib/camerad.py rename to openpilot/tools/sim/lib/camerad.py index 7634b8524d..8efb3e5dab 100644 --- a/tools/sim/lib/camerad.py +++ b/openpilot/tools/sim/lib/camerad.py @@ -1,7 +1,7 @@ import numpy as np from msgq.visionipc import VisionIpcServer, VisionStreamType -from cereal import messaging +from openpilot.cereal import messaging from openpilot.tools.sim.lib.common import W, H diff --git a/tools/sim/lib/common.py b/openpilot/tools/sim/lib/common.py similarity index 92% rename from tools/sim/lib/common.py rename to openpilot/tools/sim/lib/common.py index 1324932131..eb29119fb4 100644 --- a/tools/sim/lib/common.py +++ b/openpilot/tools/sim/lib/common.py @@ -40,7 +40,7 @@ class SimulatorState: self.is_engaged = False self.ignition = True - self.velocity: vec3 = None + self.velocity = vec3(0, 0, 0) self.bearing: float = 0 self.gps = GPSState() self.imu = IMUState() @@ -72,7 +72,7 @@ class World(ABC): self.exit_event = multiprocessing.Event() @abstractmethod - def apply_controls(self, steer_sim, throttle_out, brake_out): + def apply_controls(self, steer_sim, throttle_out, brake_out, /): pass @abstractmethod @@ -84,7 +84,7 @@ class World(ABC): pass @abstractmethod - def read_sensors(self, simulator_state: SimulatorState): + def read_sensors(self, simulator_state: SimulatorState, /): pass @abstractmethod diff --git a/tools/sim/lib/keyboard_ctrl.py b/openpilot/tools/sim/lib/keyboard_ctrl.py similarity index 100% rename from tools/sim/lib/keyboard_ctrl.py rename to openpilot/tools/sim/lib/keyboard_ctrl.py diff --git a/tools/sim/lib/manual_ctrl.py b/openpilot/tools/sim/lib/manual_ctrl.py similarity index 100% rename from tools/sim/lib/manual_ctrl.py rename to openpilot/tools/sim/lib/manual_ctrl.py diff --git a/tools/sim/lib/simulated_car.py b/openpilot/tools/sim/lib/simulated_car.py similarity index 99% rename from tools/sim/lib/simulated_car.py rename to openpilot/tools/sim/lib/simulated_car.py index 950879d394..9c430646f2 100644 --- a/tools/sim/lib/simulated_car.py +++ b/openpilot/tools/sim/lib/simulated_car.py @@ -1,5 +1,5 @@ import traceback -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from opendbc.can.packer import CANPacker from opendbc.can.parser import CANParser diff --git a/tools/sim/lib/simulated_sensors.py b/openpilot/tools/sim/lib/simulated_sensors.py similarity index 98% rename from tools/sim/lib/simulated_sensors.py rename to openpilot/tools/sim/lib/simulated_sensors.py index d6d822c582..63935d3068 100644 --- a/tools/sim/lib/simulated_sensors.py +++ b/openpilot/tools/sim/lib/simulated_sensors.py @@ -1,7 +1,7 @@ import time -from cereal import log -import cereal.messaging as messaging +from openpilot.cereal import log +import openpilot.cereal.messaging as messaging from openpilot.common.realtime import DT_DMON from openpilot.tools.sim.lib.camerad import Camerad diff --git a/tools/sim/run_bridge.py b/openpilot/tools/sim/run_bridge.py similarity index 100% rename from tools/sim/run_bridge.py rename to openpilot/tools/sim/run_bridge.py diff --git a/tools/replay/lib/__init__.py b/openpilot/tools/sim/tests/__init__.py similarity index 100% rename from tools/replay/lib/__init__.py rename to openpilot/tools/sim/tests/__init__.py diff --git a/tools/sim/tests/conftest.py b/openpilot/tools/sim/tests/conftest.py similarity index 100% rename from tools/sim/tests/conftest.py rename to openpilot/tools/sim/tests/conftest.py diff --git a/tools/sim/tests/test_metadrive_bridge.py b/openpilot/tools/sim/tests/test_metadrive_bridge.py similarity index 100% rename from tools/sim/tests/test_metadrive_bridge.py rename to openpilot/tools/sim/tests/test_metadrive_bridge.py diff --git a/tools/sim/tests/test_sim_bridge.py b/openpilot/tools/sim/tests/test_sim_bridge.py similarity index 97% rename from tools/sim/tests/test_sim_bridge.py rename to openpilot/tools/sim/tests/test_sim_bridge.py index d58cddff7f..f93cc2ef50 100644 --- a/tools/sim/tests/test_sim_bridge.py +++ b/openpilot/tools/sim/tests/test_sim_bridge.py @@ -5,11 +5,11 @@ import pytest from multiprocessing import Queue -from cereal import messaging +from openpilot.cereal import messaging from openpilot.common.basedir import BASEDIR from openpilot.tools.sim.bridge.common import QueueMessageType -SIM_DIR = os.path.join(BASEDIR, "tools/sim") +SIM_DIR = os.path.join(BASEDIR, "openpilot/tools/sim") class TestSimBridgeBase: @classmethod diff --git a/panda b/panda index d994e8e800..61b050f1bd 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit d994e8e8009d934a94c3a94c863b559118353bdd +Subproject commit 61b050f1bd36fad04d4ceccca8a72c92ee1422df diff --git a/pyproject.toml b/pyproject.toml index 8b995a3a7b..d400ff5678 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,71 +11,45 @@ authors = [ dependencies = [ # multiple users "sounddevice", # micd + soundd - "pyserial", # pigeond + qcomgpsd "requests", # many one-off uses - "sympy", # rednose + friends - "crcmod-plus", # cars + qcomgpsd "tqdm", # cars (fw_versions.py) on start + many one-off uses # core - "cffi", "scons", "pycapnp==2.1.0", # 2.2 introduces a memory leak due to cyclic references "Cython", - "setuptools", "numpy >=2.0", # vendored native dependencies - "bzip2 @ git+https://github.com/commaai/dependencies.git@release-bzip2#subdirectory=bzip2", - "bootstrap-icons @ git+https://github.com/commaai/dependencies.git@release-bootstrap-icons#subdirectory=bootstrap-icons", - "capnproto @ git+https://github.com/commaai/dependencies.git@release-capnproto#subdirectory=capnproto", - "catch2 @ git+https://github.com/commaai/dependencies.git@release-catch2#subdirectory=catch2", - "acados @ git+https://github.com/commaai/dependencies.git@release-acados#subdirectory=acados", - "eigen @ git+https://github.com/commaai/dependencies.git@release-eigen#subdirectory=eigen", - "ffmpeg @ git+https://github.com/commaai/dependencies.git@release-ffmpeg#subdirectory=ffmpeg", - "libjpeg @ git+https://github.com/commaai/dependencies.git@release-libjpeg#subdirectory=libjpeg", - "libyuv @ git+https://github.com/commaai/dependencies.git@release-libyuv#subdirectory=libyuv", - "zstd @ git+https://github.com/commaai/dependencies.git@release-zstd#subdirectory=zstd", - "ncurses @ git+https://github.com/commaai/dependencies.git@release-ncurses#subdirectory=ncurses", - "zeromq @ git+https://github.com/commaai/dependencies.git@release-zeromq#subdirectory=zeromq", - "libusb @ git+https://github.com/commaai/dependencies.git@release-libusb#subdirectory=libusb", - "json11 @ git+https://github.com/commaai/dependencies.git@release-json11#subdirectory=json11", - "git-lfs @ git+https://github.com/commaai/dependencies.git@release-git-lfs#subdirectory=git-lfs", - "gcc-arm-none-eabi @ git+https://github.com/commaai/dependencies.git@release-gcc-arm-none-eabi#subdirectory=gcc-arm-none-eabi", - "xvfb @ git+https://github.com/commaai/dependencies.git@release-xvfb#subdirectory=xvfb", - - # body / webrtcd - "av", - "aiohttp", - "aiortc", - - # panda - "libusb1", - "spidev; platform_system == 'Linux'", + "comma-deps-capnproto", + "comma-deps-catch2", + "comma-deps-acados", + "comma-deps-ffmpeg", + "comma-deps-zstd", + "comma-deps-zeromq", + "comma-deps-json11", + "comma-deps-git-lfs", + "comma-deps-gcc-arm-none-eabi", # logging "pyzmq", "sentry-sdk", - "xattr", # used in place of 'os.getxattr' for macOS compatibility # athena - "PyJWT", - "json-rpc", + "PyJWT[crypto]", "websocket_client", # joystickd "inputs", # these should be removed - "psutil", - "pycryptodome", # used in updated/casync, panda, body, and a test "setproctitle", # logreader "zstandard", # ui - "raylib @ git+https://github.com/commaai/dependencies.git@release-raylib#subdirectory=raylib", + "comma-deps-raylib", "qrcode", "jeepney", "pillow", @@ -83,7 +57,6 @@ dependencies = [ [project.optional-dependencies] docs = [ - "Jinja2", "zensical", ] @@ -99,37 +72,57 @@ testing = [ "pytest-mock", "ruff", "codespell", - "pre-commit-hooks", ] dev = [ "matplotlib", - "opencv-python-headless", ] tools = [ - "imgui @ git+https://github.com/commaai/dependencies.git@release-imgui#subdirectory=imgui", - "metadrive-simulator @ git+https://github.com/commaai/metadrive.git@minimal ; (platform_machine != 'aarch64')", + "comma-deps-imgui", + "comma-deps-bzip2", + "comma-deps-bootstrap-icons", + "comma-deps-libusb", + "comma-deps-ncurses", + + # this can be added back once it's stripped down some more + #"metadrive-simulator @ git+https://github.com/commaai/metadrive.git@minimal ; (platform_machine != 'aarch64')", +] + +submodules = [ + "msgq", + "opendbc", + "pandacan", + "rednose", + "teleoprtc", + "tinygrad", ] [project.urls] Homepage = "https://github.com/commaai/openpilot" +[dependency-groups] +standalone = [ + "openpilot[submodules]", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = [ "." ] +packages = [ + "openpilot", +] [tool.hatch.metadata] allow-direct-references = true [tool.pytest.ini_options] minversion = "6.0" -addopts = "--ignore=openpilot/ --ignore=opendbc/ --ignore=panda/ --ignore=rednose_repo/ --ignore=tinygrad_repo/ --ignore=teleoprtc_repo/ --ignore=msgq/ -Werror --strict-config --strict-markers --durations=10 -n auto --dist=loadgroup" +addopts = "-Werror --strict-config --strict-markers --durations=10 -n auto --dist=loadgroup" cpp_files = "test_*" -cpp_harness = "selfdrive/test/cpp_harness.py" +cpp_harness = "openpilot/selfdrive/test/cpp_harness.py" python_files = "test_*.py" markers = [ "slow: tests that take awhile to run and can be skipped with -m 'not slow'", @@ -140,12 +133,7 @@ markers = [ "xdist_group_class_property: group tests by a property of the class that contains them", ] testpaths = [ - "common", - "selfdrive", - "system", - "tools", - "cereal", - "sunnypilot", + "openpilot", ] [tool.codespell] @@ -153,7 +141,7 @@ quiet-level = 3 # if you've got a short variable name that's getting flagged, add it here ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints,whit,indexIn,ws,uint,grey,deque,stdio,amin,BA,LITE,atEnd,UIs,errorString,arange,FocusIn,od,tim,relA,hist,copyable,jupyter,thead,TGE,abl,lite,ser" 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, *.pem, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*, selfdrive/assets/offroad/mici_fcc.html" +skip = "*.po, uv.lock, *.onnx, *.pem, */c_generated_code/*, docs/assets/*, openpilot/tools/plotjuggler/layouts/*, openpilot/selfdrive/assets/offroad/mici_fcc.html, ./openpilot/third_party/*, ./openpilot/cereal/gen/*" # https://docs.astral.sh/ruff/configuration/#using-pyprojecttoml [tool.ruff] @@ -162,45 +150,32 @@ lint.select = [ "E", "F", "W", "PIE", "C4", "ISC", "A", "B", "NPY", # numpy "UP", # pyupgrade + "ASYNC", + "B904", "B905", + "PLC0207", "TRY203", "TRY400", "TRY401", # try/excepts - "RUF008", "RUF100", + "RUF006", "RUF008", "RUF009", "RUF061", "RUF064", "RUF100", "RUF102", "RUF103", "RUF104", "TID251", "PLE", "PLR1704", ] lint.ignore = [ "E741", "E402", - "C408", - "ISC003", "B027", - "B024", - "NPY002", # new numpy random syntax is worse - "UP045", "UP007", # these don't play nice with raylib atm + "UP007", # this doesn't play nice with raylib atm ] line-length = 160 exclude = [ - "cereal", - "panda", - "opendbc", - "opendbc_repo", - "rednose_repo", - "tinygrad_repo", - "teleoprtc", - "teleoprtc_repo", - "third_party/copyparty", + "openpilot/cereal", + "openpilot/third_party", "*.ipynb", "generated", ] lint.flake8-implicit-str-concat.allow-multiline = false [tool.ruff.lint.flake8-tidy-imports.banned-api] -"selfdrive".msg = "Use openpilot.selfdrive" -"common".msg = "Use openpilot.common" -"system".msg = "Use openpilot.system" -"tools".msg = "Use openpilot.tools" "pytest.main".msg = "pytest.main requires special handling that is easy to mess up!" -"unittest".msg = "Use pytest" -"time.time".msg = "Use time.monotonic" +"time.time".msg = "Use time.monotonic" # time.time can skip due to its reference clock, you probably want a monotonic clock # raylib banned APIs "pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure" @@ -214,31 +189,24 @@ quote-style = "preserve" [tool.ty.src] exclude = [ - "msgq/", - "msgq_repo/", - "opendbc/", - "opendbc_repo/", - "panda/", - "rednose/", - "rednose_repo/", - "tinygrad/", - "tinygrad_repo/", - "teleoprtc/", - "teleoprtc_repo/", - "third_party/", + "openpilot/third_party/", ] [tool.ty.rules] unresolved-import = "ignore" # Cython-compiled modules (.pyx) unresolved-attribute = "ignore" # many from capnp and Cython modules -invalid-method-override = "ignore" # signature variance issues -possibly-missing-attribute = "ignore" # too many false positives -invalid-assignment = "ignore" # often intentional monkey-patching -no-matching-overload = "ignore" # numpy/ctypes overload matching issues -invalid-argument-type = "ignore" # many false positives from raylib, ctypes, numpy -call-non-callable = "ignore" # false positives from dynamic types -unsupported-operator = "ignore" # false positives from dynamic types -not-subscriptable = "ignore" # false positives from dynamic types [tool.uv] python-preference = "only-managed" +default-groups = ["standalone"] +override-dependencies = [ + "opendbc", # panda pins opendbc from git for standalone use; always use our submodule +] + +[tool.uv.sources] +msgq = { path = "msgq_repo", editable = true } +opendbc = { path = "opendbc_repo", editable = true } +pandacan = { path = "panda", editable = true } +rednose = { path = "rednose_repo", editable = true } +teleoprtc = { path = "teleoprtc_repo", editable = true } +tinygrad = { path = "tinygrad_repo", editable = true } diff --git a/rednose b/rednose deleted file mode 120000 index 674cfec35f..0000000000 --- a/rednose +++ /dev/null @@ -1 +0,0 @@ -rednose_repo/rednose \ No newline at end of file diff --git a/rednose_repo b/rednose_repo index 7ffefa3d88..9e19086c26 160000 --- a/rednose_repo +++ b/rednose_repo @@ -1 +1 @@ -Subproject commit 7ffefa3d8811a842f8ec97d311103ce3a45dfae0 +Subproject commit 9e19086c26ca35708870d50ebcf237d65d0b163e diff --git a/release/ci/publish.sh b/release/ci/publish.sh index 315ae22bfe..27904caf5d 100755 --- a/release/ci/publish.sh +++ b/release/ci/publish.sh @@ -30,7 +30,7 @@ if [ -z "$GIT_ORIGIN" ]; then fi # "Tagging" -echo "#define SUNNYPILOT_VERSION \"$VERSION\"" > ${OUTPUT_DIR}/sunnypilot/common/version.h +echo "#define SUNNYPILOT_VERSION \"$VERSION\"" > ${OUTPUT_DIR}/openpilot/sunnypilot/common/version.h ## set git identity #source $DIR/identity.sh @@ -55,7 +55,7 @@ git add -f . # include source commit hash and build date in commit GIT_HASH=$(git --git-dir=$SOURCE_DIR/.git rev-parse HEAD) DATETIME=$(date '+%Y-%m-%dT%H:%M:%S') -SP_VERSION=$(awk -F\" '{print $2}' $SOURCE_DIR/sunnypilot/common/version.h) +SP_VERSION=$(awk -F\" '{print $2}' $SOURCE_DIR/openpilot/sunnypilot/common/version.h) # Commit with detailed message git commit -a -m "sunnypilot v$VERSION diff --git a/scripts/cell.sh b/scripts/cell.sh deleted file mode 100755 index 310a9694fd..0000000000 --- a/scripts/cell.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -nmcli connection modify --temporary esim ipv4.route-metric 1 ipv6.route-metric 1 -nmcli con up esim diff --git a/scripts/disable-powersave.py b/scripts/disable-powersave.py index 367b4108b0..093275fd77 100755 --- a/scripts/disable-powersave.py +++ b/scripts/disable-powersave.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE if __name__ == "__main__": HARDWARE.set_power_save(False) diff --git a/scripts/lint/check_added_large_files.py b/scripts/lint/check_added_large_files.py new file mode 100755 index 0000000000..c1aa820181 --- /dev/null +++ b/scripts/lint/check_added_large_files.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +import argparse +import math +import os +import subprocess + + +def lfs_files(filenames: list[str]) -> set[str]: + if not filenames: + return set() + + result = subprocess.run( + ("git", "check-attr", "filter", "-z", "--stdin"), + input="\0".join(filenames), + check=True, + capture_output=True, + text=True, + ) + fields = result.stdout.rstrip("\0").split("\0") if result.stdout else [] + return {fields[i] for i in range(0, len(fields), 3) if fields[i + 2] == "lfs"} + + +def check_added_large_files(filenames: list[str], max_kb: int) -> int: + failed = False + ignored = lfs_files(filenames) + for filename in filenames: + if filename in ignored: + continue + + size_kb = math.ceil(os.stat(filename).st_size / 1024) + if size_kb > max_kb: + print(f"{filename} ({size_kb} KB) exceeds {max_kb} KB.") + failed = True + + return int(failed) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Check that tracked files do not exceed a size limit.") + parser.add_argument("filenames", nargs="*") + parser.add_argument("--maxkb", type=int, default=500, help="maximum allowable size in KiB") + args = parser.parse_args() + return check_added_large_files(args.filenames, args.maxkb) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/lint/check_shebang_scripts_are_executable.py b/scripts/lint/check_shebang_scripts_are_executable.py new file mode 100755 index 0000000000..7288640091 --- /dev/null +++ b/scripts/lint/check_shebang_scripts_are_executable.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +import argparse +import shlex +import subprocess +import sys + + +def staged_modes(filenames: list[str]) -> list[tuple[str, str]]: + if not filenames: + return [] + + result = subprocess.run( + ("git", "ls-files", "-z", "--stage", "--", *filenames), + check=True, + capture_output=True, + text=True, + ) + entries = result.stdout.rstrip("\0").split("\0") if result.stdout else [] + return [(entry.split(" ", 1)[0], entry.split("\t", 1)[1]) for entry in entries] + + +def has_shebang(filename: str) -> bool: + with open(filename, "rb") as f: + return f.read(2) == b"#!" + + +def check_shebang_scripts_are_executable(filenames: list[str]) -> int: + failed = False + for mode, filename in staged_modes(filenames): + if mode != "100755" and has_shebang(filename): + quoted = shlex.quote(filename) + print("\n".join(( + f"{filename}: has a shebang but is not marked executable!", + f" If it is supposed to be executable, try: `chmod +x {quoted}`", + " If it is not supposed to be executable, double-check its shebang is wanted.\n", + )), file=sys.stderr) + failed = True + + return int(failed) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Check that tracked files with shebangs are executable.") + parser.add_argument("filenames", nargs="*") + args = parser.parse_args() + return check_shebang_scripts_are_executable(args.filenames) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/lint/lint.sh b/scripts/lint/lint.sh index 919e2ca63a..69ed3951d8 100755 --- a/scripts/lint/lint.sh +++ b/scripts/lint/lint.sh @@ -13,9 +13,6 @@ cd $ROOT FAILED=0 -IGNORED_FILES="uv\.lock|docs\/CARS.md|LICENSE\.md" -IGNORED_DIRS="^msgq.*|^msgq_repo.*|^opendbc.*|^opendbc_repo.*|^cereal.*|^panda.*|^rednose.*|^rednose_repo.*|^tinygrad.*|^tinygrad_repo.*|^teleoprtc.*|^teleoprtc_repo.*|^third_party.*" - function run() { shopt -s extglob case $1 in @@ -48,14 +45,14 @@ function run_tests() { ALL_FILES=$1 PYTHON_FILES=$2 - run "ruff" ruff check $ROOT --quiet - run "check_added_large_files" python3 -m pre_commit_hooks.check_added_large_files --enforce-all $ALL_FILES --maxkb=120 - run "check_shebang_scripts_are_executable" python3 -m pre_commit_hooks.check_shebang_scripts_are_executable $ALL_FILES + run "ruff" ruff check openpilot --quiet + run "check_added_large_files" $DIR/check_added_large_files.py --maxkb=120 $ALL_FILES + run "check_shebang_scripts_are_executable" $DIR/check_shebang_scripts_are_executable.py $ALL_FILES run "check_shebang_format" $DIR/check_shebang_format.sh $ALL_FILES run "check_nomerge_comments" $DIR/check_nomerge_comments.sh $ALL_FILES if [[ -z "$FAST" ]]; then - run "ty" ty check + run "ty" ty check openpilot run "codespell" codespell $ALL_FILES --ignore-words=$ROOT/.codespellignore fi @@ -105,7 +102,8 @@ done RUN=$([ -z "$RUN" ] && echo "" || echo "!($(echo $RUN | sed 's/ /|/g'))") SKIP="@($(echo $SKIP | sed 's/ /|/g'))" -GIT_FILES="$(git ls-files | sed -E "s/$IGNORED_FILES|$IGNORED_DIRS//g")" +IGNORED_DIRS="^openpilot/third_party/.*" +GIT_FILES="$(git ls-files openpilot | grep -vE "$IGNORED_DIRS")" ALL_FILES="" for f in $GIT_FILES; do if [[ -f $f ]]; then diff --git a/scripts/manage-powersave.py b/scripts/manage-powersave.py index 1a82810a7f..2c736ecd69 100755 --- a/scripts/manage-powersave.py +++ b/scripts/manage-powersave.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import argparse import multiprocessing -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE def main(): diff --git a/scripts/reporter.py b/scripts/reporter.py index 9821a47ccf..5de5521835 100755 --- a/scripts/reporter.py +++ b/scripts/reporter.py @@ -7,7 +7,7 @@ from tinygrad.nn.onnx import OnnxPBParser BASEDIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../")) MASTER_PATH = os.getenv("MASTER_PATH", BASEDIR) -MODEL_PATH = "/selfdrive/modeld/models/" +MODEL_PATH = "/openpilot/selfdrive/modeld/models/" class MetadataOnnxPBParser(OnnxPBParser): @@ -25,7 +25,9 @@ class MetadataOnnxPBParser(OnnxPBParser): def get_checkpoint(f): model = MetadataOnnxPBParser(f).parse() metadata = {prop["key"]: prop["value"] for prop in model["metadata_props"]} - return metadata['model_checkpoint'].split('/')[0] + # "" or "...//"; combined models list vision then policy + parts = metadata['model_checkpoint'].split('/') + return parts[-2] if len(parts) > 1 else parts[0] if __name__ == "__main__": @@ -37,8 +39,8 @@ if __name__ == "__main__": master_path = MASTER_PATH + MODEL_PATH + fn if os.path.exists(master_path): master = get_checkpoint(master_path) - master_col = f"[{master}](https://reporter.comma.life/experiment/{master})" + master_col = f"[{master}](https://reporter.comma.life/{master})" else: master_col = "N/A (new model)" pr = get_checkpoint(BASEDIR + MODEL_PATH + fn) - print("|", fn, "|", master_col, "|", f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|") + print("|", fn, "|", master_col, "|", f"[{pr}](https://reporter.comma.life/{pr})", "|") diff --git a/scripts/stop_updater.sh b/scripts/stop_updater.sh deleted file mode 100755 index 703b363928..0000000000 --- a/scripts/stop_updater.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env sh - -# Stop updater -pkill -2 -f system.updated.updated - -# Remove pending update -rm -f /data/safe_staging/finalized/.overlay_consistent diff --git a/scripts/test_pip_install.sh b/scripts/test_pip_install.sh new file mode 100755 index 0000000000..d2dbedff0a --- /dev/null +++ b/scripts/test_pip_install.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euxo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(git -C "$SCRIPT_DIR/.." rev-parse --show-toplevel)" +PYTHON_VERSION="$(cat "$REPO_ROOT/.python-version")" +PACKAGE="${1:-$REPO_ROOT}" + +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +cd "$TMPDIR" + +uv venv --python "$PYTHON_VERSION" +source .venv/bin/activate +uv pip install "$PACKAGE" +python3 - <<'PY' +from openpilot.tools.lib.logreader import LogReader + +assert LogReader.__name__ == "LogReader" +print("ok: imported LogReader") +PY diff --git a/scripts/usb.sh b/scripts/usb.sh deleted file mode 100755 index 5796cfa028..0000000000 --- a/scripts/usb.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -# testing the GPU box - -export XDG_CACHE_HOME=/data/tinycache -mkdir -p $XDG_CACHE_HOME - -cd /data/openpilot/tinygrad_repo/examples -while true; do - AMD=1 AMD_IFACE=usb python ./beautiful_cartpole.py - sleep 1 -done diff --git a/scripts/usbgpu/benchmark.sh b/scripts/usbgpu/benchmark.sh deleted file mode 100755 index 04a76d054e..0000000000 --- a/scripts/usbgpu/benchmark.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -e - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -cd $DIR/../../tinygrad_repo - -GREEN='\033[0;32m' -NC='\033[0m' - - -#export DEBUG=2 -export PYTHONPATH=. -export AM_RESET=1 -export AMD=1 -export AMD_IFACE=USB -export AMD_LLVM=1 - -python3 -m unittest -q --buffer test.test_tiny.TestTiny.test_plus \ - > /tmp/test_tiny.log 2>&1 || (cat /tmp/test_tiny.log; exit 1) -printf "${GREEN}Booted in ${SECONDS}s${NC}\n" -printf "${GREEN}=============${NC}\n" - -printf "\n\n" -printf "${GREEN}Transfer speeds:${NC}\n" -printf "${GREEN}================${NC}\n" -python3 test/external/external_test_usb_asm24.py TestDevCopySpeeds diff --git a/scripts/waste.c b/scripts/waste.c index 2e492916a7..fc230846f2 100644 --- a/scripts/waste.c +++ b/scripts/waste.c @@ -11,7 +11,7 @@ #include #include #include -#include "../common/timing.h" +#include "../openpilot/common/timing.h" int get_nprocs(void); double *ttime, *oout; @@ -86,4 +86,3 @@ int main() { sleep(1); } } - diff --git a/selfdrive/assets/sounds/disengage.wav b/selfdrive/assets/sounds/disengage.wav deleted file mode 100644 index 7bfd97ad71..0000000000 --- a/selfdrive/assets/sounds/disengage.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:42bd04a57b527c787a0555503e02a203f7d672c12d448769a3f41f17befbf013 -size 48044 diff --git a/selfdrive/assets/sounds/engage.wav b/selfdrive/assets/sounds/engage.wav deleted file mode 100644 index 8633b5ac2d..0000000000 --- a/selfdrive/assets/sounds/engage.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b1e177499d9439367179cc57a6301b6162393972e3a136cc35c5fdac026bf10a -size 48044 diff --git a/selfdrive/assets/sounds/prompt.wav b/selfdrive/assets/sounds/prompt.wav deleted file mode 100644 index e482c85a62..0000000000 --- a/selfdrive/assets/sounds/prompt.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ad19268e4aaaeac8dd21f6b26c16a121e7b3f50bba867748e7226727643ae682 -size 144642 diff --git a/selfdrive/assets/sounds/prompt_distracted.wav b/selfdrive/assets/sounds/prompt_distracted.wav deleted file mode 100644 index 750d580f04..0000000000 --- a/selfdrive/assets/sounds/prompt_distracted.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1810ad0418ac234f02dec005883c8f0e1c3e0e5ece7a3157803c5a66cb8e5adc -size 85662 diff --git a/selfdrive/assets/sounds/refuse.wav b/selfdrive/assets/sounds/refuse.wav deleted file mode 100644 index 1e0c47697d..0000000000 --- a/selfdrive/assets/sounds/refuse.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4af81cbf1d96a42cc351878b015298aee82874b46baaf1a615ca91ec36c0ced6 -size 83228 diff --git a/selfdrive/assets/sounds/warning_immediate.wav b/selfdrive/assets/sounds/warning_immediate.wav deleted file mode 100644 index fcbfed79ed..0000000000 --- a/selfdrive/assets/sounds/warning_immediate.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5a390831afca3bfc6ea3c2739b872ebf866e70df8ae30653f8587e5cd3993959 -size 68306 diff --git a/selfdrive/assets/sounds/warning_soft.wav b/selfdrive/assets/sounds/warning_soft.wav deleted file mode 100644 index 7db30303d6..0000000000 --- a/selfdrive/assets/sounds/warning_soft.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:67e636d072703e6b1233a12344c0a6304fd43d64dbb31c66b71c2c8870a339c1 -size 153764 diff --git a/selfdrive/car/tests/big_cars_test.sh b/selfdrive/car/tests/big_cars_test.sh deleted file mode 100755 index bb6e82dd0e..0000000000 --- a/selfdrive/car/tests/big_cars_test.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -SCRIPT_DIR=$(dirname "$0") -BASEDIR=$(realpath "$SCRIPT_DIR/../../../") -cd $BASEDIR - -export MAX_EXAMPLES=300 -export INTERNAL_SEG_CNT=300 -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/car/tests/test_docs.py b/selfdrive/car/tests/test_docs.py deleted file mode 100644 index 6e13d55b29..0000000000 --- a/selfdrive/car/tests/test_docs.py +++ /dev/null @@ -1,22 +0,0 @@ -import os - -from openpilot.common.basedir import BASEDIR -from opendbc.car.docs import generate_cars_md, get_all_car_docs -from openpilot.selfdrive.debug.dump_car_docs import dump_car_docs -from openpilot.selfdrive.debug.print_docs_diff import print_car_docs_diff -from openpilot.selfdrive.car.docs import CARS_MD_TEMPLATE - - -class TestCarDocs: - @classmethod - def setup_class(cls): - cls.all_cars = get_all_car_docs() - - def test_generator(self): - generate_cars_md(self.all_cars, CARS_MD_TEMPLATE) - - def test_docs_diff(self): - dump_path = os.path.join(BASEDIR, "selfdrive", "car", "tests", "cars_dump") - dump_car_docs(dump_path) - print_car_docs_diff(dump_path) - os.remove(dump_path) diff --git a/selfdrive/controls/lib/lateral_mpc_lib/.gitignore b/selfdrive/controls/lib/lateral_mpc_lib/.gitignore deleted file mode 100644 index aff2eb082b..0000000000 --- a/selfdrive/controls/lib/lateral_mpc_lib/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -acados_ocp_lat.json -c_generated_code/ diff --git a/selfdrive/controls/lib/lateral_mpc_lib/SConscript b/selfdrive/controls/lib/lateral_mpc_lib/SConscript deleted file mode 100644 index 5ff526ae83..0000000000 --- a/selfdrive/controls/lib/lateral_mpc_lib/SConscript +++ /dev/null @@ -1,104 +0,0 @@ -Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'np_version', 'acados') - -gen = "c_generated_code" - -casadi_model = [ - f'{gen}/lat_model/lat_expl_ode_fun.c', - f'{gen}/lat_model/lat_expl_vde_forw.c', -] - -casadi_cost_y = [ - f'{gen}/lat_cost/lat_cost_y_fun.c', - f'{gen}/lat_cost/lat_cost_y_fun_jac_ut_xt.c', - f'{gen}/lat_cost/lat_cost_y_hess.c', -] - -casadi_cost_e = [ - f'{gen}/lat_cost/lat_cost_y_e_fun.c', - f'{gen}/lat_cost/lat_cost_y_e_fun_jac_ut_xt.c', - f'{gen}/lat_cost/lat_cost_y_e_hess.c', -] - -casadi_cost_0 = [ - f'{gen}/lat_cost/lat_cost_y_0_fun.c', - f'{gen}/lat_cost/lat_cost_y_0_fun_jac_ut_xt.c', - f'{gen}/lat_cost/lat_cost_y_0_hess.c', -] - -build_files = [f'{gen}/acados_solver_lat.c'] + casadi_model + casadi_cost_y + casadi_cost_e + casadi_cost_0 - -# extra generated files used to trigger a rebuild -generated_files = [ - f'{gen}/Makefile', - - f'{gen}/main_lat.c', - f'{gen}/main_sim_lat.c', - f'{gen}/acados_solver_lat.h', - f'{gen}/acados_sim_solver_lat.h', - f'{gen}/acados_sim_solver_lat.c', - f'{gen}/acados_solver.pxd', - - f'{gen}/lat_model/lat_expl_vde_adj.c', - - f'{gen}/lat_model/lat_model.h', - f'{gen}/lat_constraints/lat_constraints.h', - f'{gen}/lat_cost/lat_cost.h', -] + build_files - -acados_include_dir = Dir(acados.INCLUDE_DIR) -acados_template_dir = Dir(acados.TEMPLATE_DIR) - -source_list = ['lat_mpc.py', - '#selfdrive/modeld/constants.py', - acados_include_dir.File('acados_c/ocp_nlp_interface.h'), - acados_template_dir.File('c_templates_tera/acados_solver.in.c'), -] - -lenv = env.Clone() -copied_acados_libs = [] -if arch != "Darwin": - for lib in ["libacados.so", "libblasfeo.so", "libhpipm.so", "libqpOASES_e.so.3.1"]: - copied_acados_libs += lenv.Command(f"{gen}/{lib}", Dir(acados.LIB_DIR).File(lib), [Mkdir(Dir(gen)), Copy("$TARGET", "$SOURCE")]) - lenv["RPATH"] += [lenv.Literal('\\$$ORIGIN')] -else: - acados_rel_path = Dir(gen).rel_path(Dir(acados.LIB_DIR)) - lenv["RPATH"] += [lenv.Literal(f'\\$$ORIGIN/{acados_rel_path}')] -lenv.Clean(generated_files, Dir(gen)) - -generated_lat = lenv.Command(generated_files, - source_list, - f"cd {Dir('.').abspath} && python3 lat_mpc.py") -lenv.Depends(generated_lat, [msgq_python, common_python]) - -lenv["CFLAGS"].append("-DACADOS_WITH_QPOASES") -lenv["CXXFLAGS"].append("-DACADOS_WITH_QPOASES") -lenv["CCFLAGS"].append("-Wno-unused") -if arch != "Darwin": - lenv["LINKFLAGS"].append("-Wl,--disable-new-dtags") -else: - lenv["LINKFLAGS"].append("-Wl,-install_name,@loader_path/libacados_ocp_solver_lat.dylib") - lenv["LINKFLAGS"].append(f"-Wl,-rpath,@loader_path/{acados_rel_path}") -lib_solver = lenv.SharedLibrary(f"{gen}/acados_ocp_solver_lat", - build_files, - LIBS=['m', 'acados', 'hpipm', 'blasfeo', 'qpOASES_e']) - -# generate cython stuff -acados_ocp_solver_pyx = acados_template_dir.File('acados_ocp_solver_pyx.pyx') -acados_ocp_solver_common = acados_template_dir.File('acados_solver_common.pxd') -libacados_ocp_solver_pxd = File(f'{gen}/acados_solver.pxd') -libacados_ocp_solver_c = File(f'{gen}/acados_ocp_solver_pyx.c') - -lenv2 = envCython.Clone() -lenv2["LIBPATH"] += [lib_solver[0].dir.abspath] -lenv2["RPATH"] += [lenv2.Literal('\\$$ORIGIN')] -lenv2.Command(libacados_ocp_solver_c, - [acados_ocp_solver_pyx, acados_ocp_solver_common, libacados_ocp_solver_pxd], - f'cython' + \ - f' -o {libacados_ocp_solver_c.get_labspath()}' + \ - f' -I {libacados_ocp_solver_pxd.get_dir().get_labspath()}' + \ - f' -I {acados_ocp_solver_common.get_dir().get_labspath()}' + \ - f' {acados_ocp_solver_pyx.get_labspath()}') -lib_cython = lenv2.Program(f'{gen}/acados_ocp_solver_pyx.so', [libacados_ocp_solver_c], LIBS=['acados_ocp_solver_lat']) -lenv2.Depends(lib_cython, lib_solver) -lenv2.Depends(lib_cython, copied_acados_libs) -lenv2.Depends(libacados_ocp_solver_c, np_version) diff --git a/selfdrive/controls/lib/lateral_mpc_lib/lat_mpc.py b/selfdrive/controls/lib/lateral_mpc_lib/lat_mpc.py deleted file mode 100755 index 3b597b2e42..0000000000 --- a/selfdrive/controls/lib/lateral_mpc_lib/lat_mpc.py +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/env python3 -import os -import time -import numpy as np - -from casadi import SX, vertcat, sin, cos -# WARNING: imports outside of constants will not trigger a rebuild -from openpilot.selfdrive.modeld.constants import ModelConstants - -if __name__ == '__main__': # generating code - from acados.acados_template import AcadosModel, AcadosOcp, AcadosOcpSolver -else: - from openpilot.selfdrive.controls.lib.lateral_mpc_lib.c_generated_code.acados_ocp_solver_pyx import AcadosOcpSolverCython - -LAT_MPC_DIR = os.path.dirname(os.path.abspath(__file__)) -EXPORT_DIR = os.path.join(LAT_MPC_DIR, "c_generated_code") -JSON_FILE = os.path.join(LAT_MPC_DIR, "acados_ocp_lat.json") -X_DIM = 4 -P_DIM = 2 -COST_E_DIM = 3 -COST_DIM = COST_E_DIM + 2 -SPEED_OFFSET = 10.0 -MODEL_NAME = 'lat' -ACADOS_SOLVER_TYPE = 'SQP_RTI' -N = 32 - -def gen_lat_model(): - model = AcadosModel() - model.name = MODEL_NAME - - # set up states & controls - x_ego = SX.sym('x_ego') - y_ego = SX.sym('y_ego') - psi_ego = SX.sym('psi_ego') - psi_rate_ego = SX.sym('psi_rate_ego') - model.x = vertcat(x_ego, y_ego, psi_ego, psi_rate_ego) - - # parameters - v_ego = SX.sym('v_ego') - rotation_radius = SX.sym('rotation_radius') - model.p = vertcat(v_ego, rotation_radius) - - # controls - psi_accel_ego = SX.sym('psi_accel_ego') - model.u = vertcat(psi_accel_ego) - - # xdot - x_ego_dot = SX.sym('x_ego_dot') - y_ego_dot = SX.sym('y_ego_dot') - psi_ego_dot = SX.sym('psi_ego_dot') - psi_rate_ego_dot = SX.sym('psi_rate_ego_dot') - - model.xdot = vertcat(x_ego_dot, y_ego_dot, psi_ego_dot, psi_rate_ego_dot) - - # dynamics model - f_expl = vertcat(v_ego * cos(psi_ego) - rotation_radius * sin(psi_ego) * psi_rate_ego, - v_ego * sin(psi_ego) + rotation_radius * cos(psi_ego) * psi_rate_ego, - psi_rate_ego, - psi_accel_ego) - model.f_impl_expr = model.xdot - f_expl - model.f_expl_expr = f_expl - return model - - -def gen_lat_ocp(): - ocp = AcadosOcp() - ocp.model = gen_lat_model() - - Tf = np.array(ModelConstants.T_IDXS)[N] - - # set dimensions - ocp.dims.N = N - - # set cost module - ocp.cost.cost_type = 'NONLINEAR_LS' - ocp.cost.cost_type_e = 'NONLINEAR_LS' - - Q = np.diag(np.zeros(COST_E_DIM)) - QR = np.diag(np.zeros(COST_DIM)) - - ocp.cost.W = QR - ocp.cost.W_e = Q - - y_ego, psi_ego, psi_rate_ego = ocp.model.x[1], ocp.model.x[2], ocp.model.x[3] - psi_rate_ego_dot = ocp.model.u[0] - v_ego = ocp.model.p[0] - - ocp.parameter_values = np.zeros((P_DIM, )) - - ocp.cost.yref = np.zeros((COST_DIM, )) - ocp.cost.yref_e = np.zeros((COST_E_DIM, )) - # Add offset to smooth out low speed control - # TODO unclear if this right solution long term - v_ego_offset = v_ego + SPEED_OFFSET - # TODO there are two costs on psi_rate_ego_dot, one - # is correlated to jerk the other to steering wheel movement - # the steering wheel movement cost is added to prevent excessive - # wheel movements - ocp.model.cost_y_expr = vertcat(y_ego, - v_ego_offset * psi_ego, - v_ego_offset * psi_rate_ego, - v_ego_offset * psi_rate_ego_dot, - psi_rate_ego_dot / (v_ego + 0.1)) - ocp.model.cost_y_expr_e = vertcat(y_ego, - v_ego_offset * psi_ego, - v_ego_offset * psi_rate_ego) - - # set constraints - ocp.constraints.constr_type = 'BGH' - ocp.constraints.idxbx = np.array([2,3]) - ocp.constraints.ubx = np.array([np.radians(90), np.radians(50)]) - ocp.constraints.lbx = np.array([-np.radians(90), -np.radians(50)]) - x0 = np.zeros((X_DIM,)) - ocp.constraints.x0 = x0 - - ocp.solver_options.qp_solver = 'PARTIAL_CONDENSING_HPIPM' - ocp.solver_options.hessian_approx = 'GAUSS_NEWTON' - ocp.solver_options.integrator_type = 'ERK' - ocp.solver_options.nlp_solver_type = ACADOS_SOLVER_TYPE - ocp.solver_options.qp_solver_iter_max = 1 - ocp.solver_options.qp_solver_cond_N = 1 - - # set prediction horizon - ocp.solver_options.tf = Tf - ocp.solver_options.shooting_nodes = np.array(ModelConstants.T_IDXS)[:N+1] - - ocp.code_export_directory = EXPORT_DIR - return ocp - - -class LateralMpc: - def __init__(self, x0=None): - if x0 is None: - x0 = np.zeros(X_DIM) - self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N) - self.reset(x0) - - def reset(self, x0=None): - if x0 is None: - x0 = np.zeros(X_DIM) - self.x_sol = np.zeros((N+1, X_DIM)) - self.u_sol = np.zeros((N, 1)) - self.yref = np.zeros((N+1, 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]) - - # Somehow needed for stable init - for i in range(N+1): - self.solver.set(i, 'x', np.zeros(X_DIM)) - self.solver.set(i, 'p', np.zeros(P_DIM)) - self.solver.constraints_set(0, "lbx", x0) - self.solver.constraints_set(0, "ubx", x0) - self.solver.solve() - self.solution_status = 0 - self.solve_time = 0.0 - self.cost = 0 - - def set_weights(self, path_weight, heading_weight, - lat_accel_weight, lat_jerk_weight, - steering_rate_weight): - W = np.asfortranarray(np.diag([path_weight, heading_weight, - lat_accel_weight, lat_jerk_weight, - steering_rate_weight])) - for i in range(N): - self.solver.cost_set(i, 'W', W) - self.solver.cost_set(N, 'W', W[:COST_E_DIM,:COST_E_DIM]) - - def run(self, x0, p, y_pts, heading_pts, yaw_rate_pts): - x0_cp = np.copy(x0) - p_cp = np.copy(p) - self.solver.constraints_set(0, "lbx", x0_cp) - self.solver.constraints_set(0, "ubx", x0_cp) - self.yref[:,0] = y_pts - v_ego = p_cp[0, 0] - # rotation_radius = p_cp[1] - self.yref[:,1] = heading_pts * (v_ego + SPEED_OFFSET) - self.yref[:,2] = yaw_rate_pts * (v_ego + SPEED_OFFSET) - for i in range(N): - self.solver.cost_set(i, "yref", self.yref[i]) - self.solver.set(i, "p", p_cp[i]) - self.solver.set(N, "p", p_cp[N]) - self.solver.cost_set(N, "yref", self.yref[N][:COST_E_DIM]) - - t = time.monotonic() - self.solution_status = self.solver.solve() - self.solve_time = time.monotonic() - t - - for i in range(N+1): - self.x_sol[i] = self.solver.get(i, 'x') - for i in range(N): - self.u_sol[i] = self.solver.get(i, 'u') - self.cost = self.solver.get_cost() - - -if __name__ == "__main__": - ocp = gen_lat_ocp() - AcadosOcpSolver.generate(ocp, json_file=JSON_FILE) - # AcadosOcpSolver.build(ocp.code_export_directory, with_cython=True) diff --git a/selfdrive/controls/tests/test_lateral_mpc.py b/selfdrive/controls/tests/test_lateral_mpc.py deleted file mode 100644 index 3aa0fd1bce..0000000000 --- a/selfdrive/controls/tests/test_lateral_mpc.py +++ /dev/null @@ -1,85 +0,0 @@ -import pytest -import numpy as np -from openpilot.selfdrive.controls.lib.lateral_mpc_lib.lat_mpc import LateralMpc -from openpilot.selfdrive.controls.lib.drive_helpers import CAR_ROTATION_RADIUS -from openpilot.selfdrive.controls.lib.lateral_mpc_lib.lat_mpc import N as LAT_MPC_N - - -def run_mpc(lat_mpc=None, v_ref=30., x_init=0., y_init=0., psi_init=0., curvature_init=0., - lane_width=3.6, poly_shift=0.): - - if lat_mpc is None: - lat_mpc = LateralMpc() - lat_mpc.set_weights(1., .1, 0.0, .05, 800) - - y_pts = poly_shift * np.ones(LAT_MPC_N + 1) - heading_pts = np.zeros(LAT_MPC_N + 1) - curv_rate_pts = np.zeros(LAT_MPC_N + 1) - - x0 = np.array([x_init, y_init, psi_init, curvature_init]) - p = np.column_stack([v_ref * np.ones(LAT_MPC_N + 1), - CAR_ROTATION_RADIUS * np.ones(LAT_MPC_N + 1)]) - - # converge in no more than 10 iterations - for _ in range(10): - lat_mpc.run(x0, p, - y_pts, heading_pts, curv_rate_pts) - return lat_mpc.x_sol - - -class TestLateralMpc: - - def _assert_null(self, sol, curvature=1e-6): - for i in range(len(sol)): - assert sol[0,i,1] == pytest.approx(0, abs=curvature) - assert sol[0,i,2] == pytest.approx(0, abs=curvature) - assert sol[0,i,3] == pytest.approx(0, abs=curvature) - - def _assert_simmetry(self, sol, curvature=1e-6): - for i in range(len(sol)): - assert sol[0,i,1] == pytest.approx(-sol[1,i,1], abs=curvature) - assert sol[0,i,2] == pytest.approx(-sol[1,i,2], abs=curvature) - assert sol[0,i,3] == pytest.approx(-sol[1,i,3], abs=curvature) - assert sol[0,i,0] == pytest.approx(sol[1,i,0], abs=curvature) - - def test_straight(self): - sol = run_mpc() - self._assert_null(np.array([sol])) - - def test_y_symmetry(self): - sol = [] - for y_init in [-0.5, 0.5]: - sol.append(run_mpc(y_init=y_init)) - self._assert_simmetry(np.array(sol)) - - def test_poly_symmetry(self): - sol = [] - for poly_shift in [-1., 1.]: - sol.append(run_mpc(poly_shift=poly_shift)) - self._assert_simmetry(np.array(sol)) - - def test_curvature_symmetry(self): - sol = [] - for curvature_init in [-0.1, 0.1]: - sol.append(run_mpc(curvature_init=curvature_init)) - self._assert_simmetry(np.array(sol)) - - def test_psi_symmetry(self): - sol = [] - for psi_init in [-0.1, 0.1]: - sol.append(run_mpc(psi_init=psi_init)) - self._assert_simmetry(np.array(sol)) - - def test_no_overshoot(self): - y_init = 1. - sol = run_mpc(y_init=y_init) - for y in list(sol[:,1]): - assert y_init >= abs(y) - - def test_switch_convergence(self): - lat_mpc = LateralMpc() - sol = run_mpc(lat_mpc=lat_mpc, poly_shift=3.0, v_ref=7.0) - right_psi_deg = np.degrees(sol[:,2]) - sol = run_mpc(lat_mpc=lat_mpc, poly_shift=-3.0, v_ref=7.0) - left_psi_deg = np.degrees(sol[:,2]) - np.testing.assert_almost_equal(right_psi_deg, -left_psi_deg, decimal=3) diff --git a/selfdrive/debug/README.md b/selfdrive/debug/README.md deleted file mode 100644 index 83b8a994db..0000000000 --- a/selfdrive/debug/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# debug scripts - -## [can_printer.py](can_printer.py) - -``` -usage: can_printer.py [-h] [--bus BUS] [--max_msg MAX_MSG] [--addr ADDR] - -simple CAN data viewer - -optional arguments: - -h, --help show this help message and exit - --bus BUS CAN bus to print out (default: 0) - --max_msg MAX_MSG max addr (default: None) - --addr ADDR -``` - -## [dump.py](dump.py) - -``` -usage: dump.py [-h] [--pipe] [--raw] [--json] [--dump-json] [--no-print] [--addr ADDR] [--values VALUES] [socket [socket ...]] - -Dump communication sockets. See cereal/services.py for a complete list of available sockets. - -positional arguments: - socket socket names to dump. defaults to all services defined in cereal - -optional arguments: - -h, --help show this help message and exit - --pipe - --raw - --json - --dump-json - --no-print - --addr ADDR - --values VALUES values to monitor (instead of entire event) -``` - -## [vw_mqb_config.py](vw_mqb_config.py) - -``` -usage: vw_mqb_config.py [-h] [--debug] {enable,show,disable} - -Shows Volkswagen EPS software and coding info, and enables or disables Heading Control -Assist (Lane Assist). Useful for enabling HCA on cars without factory Lane Assist that want -to use openpilot integrated at the CAN gateway (J533). - -positional arguments: - {enable,show,disable} - show or modify current EPS HCA config - -optional arguments: - -h, --help show this help message and exit - --debug enable ISO-TP/UDS stack debugging output - -This tool is meant to run directly on a vehicle-installed comma three, with -the openpilot/tmux processes stopped. It should also work on a separate PC with a USB- -attached comma panda. Vehicle ignition must be on. Recommend engine not be running when -making changes. Must turn ignition off and on again for any changes to take effect. -``` diff --git a/selfdrive/debug/analyze-msg-size.py b/selfdrive/debug/analyze-msg-size.py deleted file mode 100755 index 69015a6be2..0000000000 --- a/selfdrive/debug/analyze-msg-size.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -import argparse -from tqdm import tqdm - -from cereal.services import SERVICE_LIST, QueueSize -from openpilot.tools.lib.logreader import LogReader - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Analyze message sizes from a log route") - parser.add_argument("route", nargs="?", default="98395b7c5b27882e/000000a8--f87e7cd255", - help="Log route to analyze (default: 98395b7c5b27882e/000000a8--f87e7cd255)") - args = parser.parse_args() - - lr = LogReader(args.route) - - szs = {} - for msg in tqdm(lr): - sz = len(msg.as_builder().to_bytes()) - msg_type = msg.which() - if msg_type not in szs: - szs[msg_type] = {'min': sz, 'max': sz, 'sum': sz, 'count': 1} - else: - szs[msg_type]['min'] = min(szs[msg_type]['min'], sz) - szs[msg_type]['max'] = max(szs[msg_type]['max'], sz) - szs[msg_type]['sum'] += sz - szs[msg_type]['count'] += 1 - - print() - print(f"{'Service':<36} {'Min (KB)':>12} {'Max (KB)':>12} {'Avg (KB)':>12} {'KB/min':>12} {'KB/sec':>12} {'Minutes in 10MB':>18} {'Seconds in Queue':>18}") - print("-" * 132) - def sort_key(x): - k, v = x - avg = v['sum'] / v['count'] - freq = SERVICE_LIST.get(k, None) - freq_val = freq.frequency if freq else 0.0 - kb_per_min = (avg * freq_val * 60) / 1024 if freq_val > 0 else 0.0 - return kb_per_min - total_kb_per_min = 0.0 - RINGBUFFER_SIZE_KB = 10 * 1024 # 10MB old default - for k, v in sorted(szs.items(), key=sort_key, reverse=True): - avg = v['sum'] / v['count'] - service = SERVICE_LIST.get(k, None) - freq_val = service.frequency if service else 0.0 - queue_size_kb = (service.queue_size / 1024) if service else 250 # default to SMALL - kb_per_min = (avg * freq_val * 60) / 1024 if freq_val > 0 else 0.0 - kb_per_sec = kb_per_min / 60 - minutes_in_buffer = RINGBUFFER_SIZE_KB / kb_per_min if kb_per_min > 0 else float('inf') - seconds_in_queue = (queue_size_kb / kb_per_sec) if kb_per_sec > 0 else float('inf') - total_kb_per_min += kb_per_min - min_str = f"{minutes_in_buffer:.2f}" if minutes_in_buffer != float('inf') else "inf" - sec_queue_str = f"{seconds_in_queue:.2f}" if seconds_in_queue != float('inf') else "inf" - print(f"{k:<36} {v['min']/1024:>12.2f} {v['max']/1024:>12.2f} {avg/1024:>12.2f} {kb_per_min:>12.2f} {kb_per_sec:>12.2f} {min_str:>18} {sec_queue_str:>18}") - - # Summary section - print() - print(f"Total usage: {total_kb_per_min / 1024:.2f} MB/min") - - # Calculate memory usage: old (10MB for all) vs new (from services.py) - OLD_SIZE = 10 * 1024 * 1024 # 10MB was the old default - old_total = len(SERVICE_LIST) * OLD_SIZE - - new_total = sum(s.queue_size for s in SERVICE_LIST.values()) - - # Count by queue size - size_counts = {QueueSize.BIG: 0, QueueSize.MEDIUM: 0, QueueSize.SMALL: 0} - for s in SERVICE_LIST.values(): - size_counts[s.queue_size] += 1 - - savings_pct = (1 - new_total / old_total) * 100 - - print() - print(f"{'Queue Size Comparison':<40}") - print("-" * 60) - print(f"{'Old (10MB default):':<30} {old_total / 1024 / 1024:>10.2f} MB") - print(f"{'New (from services.py):':<30} {new_total / 1024 / 1024:>10.2f} MB") - print(f"{'Savings:':<30} {savings_pct:>10.1f}%") - print() - print(f"{'Breakdown:':<30}") - print(f" BIG (10MB): {size_counts[QueueSize.BIG]:>3} services") - print(f" MEDIUM (2MB): {size_counts[QueueSize.MEDIUM]:>3} services") - print(f" SMALL (250KB): {size_counts[QueueSize.SMALL]:>3} services") diff --git a/selfdrive/debug/check_can_parser_performance.py b/selfdrive/debug/check_can_parser_performance.py deleted file mode 100755 index 20987b3cf4..0000000000 --- a/selfdrive/debug/check_can_parser_performance.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -import numpy as np -import time -from tqdm import tqdm - -from cereal import car -from opendbc.car.tests.routes import CarTestRoute -from openpilot.selfdrive.car.tests.test_models import TestCarModelBase -from openpilot.tools.plotjuggler.juggle import DEMO_ROUTE - -N_RUNS = 10 - - -class CarModelTestCase(TestCarModelBase): - test_route = CarTestRoute(DEMO_ROUTE, None) - - -if __name__ == '__main__': - # Get CAN messages and parsers - tm = CarModelTestCase() - tm.setUpClass() - tm.setUp() - - CC = car.CarControl.new_message() - ets = [] - for _ in tqdm(range(N_RUNS)): - start_t = time.process_time_ns() - for msg in tm.can_msgs: - for cp in tm.CI.can_parsers.values(): - if cp is not None: - cp.update_strings(msg) - ets.append((time.process_time_ns() - start_t) * 1e-6) - - print(f'{len(tm.can_msgs)} CAN packets, {N_RUNS} runs') - print(f'{np.mean(ets):.2f} mean ms, {max(ets):.2f} max ms, {min(ets):.2f} min ms, {np.std(ets):.2f} std ms') - print(f'{np.mean(ets) / len(tm.can_msgs):.4f} mean ms / CAN packet') diff --git a/selfdrive/debug/check_freq.py b/selfdrive/debug/check_freq.py deleted file mode 100755 index 1765aeb86b..0000000000 --- a/selfdrive/debug/check_freq.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import numpy as np -import time -from collections import defaultdict, deque -from collections.abc import MutableSequence - -import cereal.messaging as messaging - - -if __name__ == "__main__": - context = messaging.Context() - poller = messaging.Poller() - - parser = argparse.ArgumentParser() - parser.add_argument("socket", type=str, nargs='*', help="socket name") - args = parser.parse_args() - - socket_names = args.socket - sockets = {} - - rcv_times: defaultdict[str, MutableSequence[float]] = defaultdict(lambda: deque(maxlen=100)) - valids: defaultdict[str, deque[bool]] = defaultdict(lambda: deque(maxlen=100)) - - t = time.monotonic() - for name in socket_names: - sock = messaging.sub_sock(name, poller=poller) - sockets[sock] = name - - prev_print = t - while True: - for socket in poller.poll(100): - msg = messaging.recv_one(socket) - if msg is None: - continue - - name = msg.which() - - t = time.monotonic() - rcv_times[name].append(msg.logMonoTime / 1e9) - valids[name].append(msg.valid) - - if t - prev_print > 1: - print() - for name in socket_names: - dts = np.diff(rcv_times[name]) - mean = np.mean(dts) - print(f"{name}: Freq {1.0 / mean:.2f} Hz, Min {np.min(dts) / mean * 100:.2f}%, Max {np.max(dts) / mean * 100:.2f}%, valid ", all(valids[name])) - - prev_print = t diff --git a/selfdrive/debug/check_lag.py b/selfdrive/debug/check_lag.py deleted file mode 100755 index 341ae79c8b..0000000000 --- a/selfdrive/debug/check_lag.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 - -import cereal.messaging as messaging -from cereal.services import SERVICE_LIST - -TO_CHECK = ['carState'] - - -if __name__ == "__main__": - sm = messaging.SubMaster(TO_CHECK) - - prev_t: dict[str, float] = {} - - while True: - sm.update() - - for s in TO_CHECK: - if sm.updated[s]: - t = sm.logMonoTime[s] / 1e9 - - if s in prev_t: - expected = 1.0 / (SERVICE_LIST[s].frequency) - dt = t - prev_t[s] - if dt > 10 * expected: - print(t, s, dt) - - prev_t[s] = t diff --git a/selfdrive/debug/check_timings.py b/selfdrive/debug/check_timings.py deleted file mode 100755 index fc527cd81b..0000000000 --- a/selfdrive/debug/check_timings.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -import sys -import time -import numpy as np -import datetime -from collections.abc import MutableSequence -from collections import defaultdict - -import cereal.messaging as messaging - - -if __name__ == "__main__": - ts: defaultdict[str, MutableSequence[float]] = defaultdict(list) - socks = {s: messaging.sub_sock(s, conflate=False) for s in sys.argv[1:]} - try: - st = time.monotonic() - while True: - print() - for s, sock in socks.items(): - msgs = messaging.drain_sock(sock) - for m in msgs: - ts[s].append(m.logMonoTime / 1e6) - - if len(ts[s]) > 2: - d = np.diff(ts[s])[-100:] - print(f"{s:25} {np.mean(d):7.2f} {np.std(d):7.2f} {np.max(d):7.2f} {np.min(d):7.2f}") - time.sleep(1) - except KeyboardInterrupt: - print("\n") - print("="*5, "timing summary", "="*5) - for s, sock in socks.items(): - msgs = messaging.drain_sock(sock) - if len(ts[s]) > 2: - d = np.diff(ts[s]) - print(f"{s:25} {np.mean(d):7.2f} {np.std(d):7.2f} {np.max(d):7.2f} {np.min(d):7.2f}") - print("="*5, datetime.timedelta(seconds=time.monotonic()-st), "="*5) diff --git a/selfdrive/debug/cpu_usage_stat.py b/selfdrive/debug/cpu_usage_stat.py deleted file mode 100755 index 089685103f..0000000000 --- a/selfdrive/debug/cpu_usage_stat.py +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python3 -''' -System tools like top/htop can only show current cpu usage values, so I write this script to do statistics jobs. - Features: - Use psutil library to sample cpu usage(avergage for all cores) of openpilot processes, at a rate of 5 samples/sec. - Do cpu usage statistics periodically, 5 seconds as a cycle. - Calculate the average cpu usage within this cycle. - Calculate minumium/maximum/accumulated_average cpu usage as long term inspections. - Monitor multiple processes simuteneously. - Sample usage: - root@localhost:/data/openpilot$ python selfdrive/debug/cpu_usage_stat.py pandad,ubloxd - ('Add monitored proc:', './pandad') - ('Add monitored proc:', 'python locationd/ubloxd.py') - pandad: 1.96%, min: 1.96%, max: 1.96%, acc: 1.96% - ubloxd.py: 0.39%, min: 0.39%, max: 0.39%, acc: 0.39% -''' -import psutil -import time -import os -import sys -import numpy as np -import argparse -import re -from collections import defaultdict - -from openpilot.system.manager.process_config import managed_processes - -# Do statistics every 5 seconds -PRINT_INTERVAL = 5 -SLEEP_INTERVAL = 0.2 - -monitored_proc_names = [ - # android procs - 'SurfaceFlinger', 'sensors.qcom' -] + list(managed_processes.keys()) - -cpu_time_names = ['user', 'system', 'children_user', 'children_system'] - - -def get_arg_parser(): - parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) - - parser.add_argument("proc_names", nargs="?", default='', - help="Process names to be monitored, comma separated") - parser.add_argument("--list_all", action='store_true', - help="Show all running processes' cmdline") - parser.add_argument("--detailed_times", action='store_true', - help="show cpu time details (split by user, system, child user, child system)") - return parser - - -if __name__ == "__main__": - args = get_arg_parser().parse_args(sys.argv[1:]) - if args.list_all: - for p in psutil.process_iter(): - print('cmdline', p.cmdline(), 'name', p.name()) - sys.exit(0) - - if len(args.proc_names) > 0: - monitored_proc_names = args.proc_names.split(',') - monitored_procs = [] - stats = {} - for p in psutil.process_iter(): - if p == psutil.Process(): - continue - matched = any(l for l in p.cmdline() if any(pn for pn in monitored_proc_names if re.match(fr'.*{pn}.*', l, re.M | re.I))) - if matched: - k = ' '.join(p.cmdline()) - print('Add monitored proc:', k) - stats[k] = {'cpu_samples': defaultdict(list), 'min': defaultdict(lambda: None), 'max': defaultdict(lambda: None), - 'avg': defaultdict(float), 'last_cpu_times': None, 'last_sys_time': None} - stats[k]['last_sys_time'] = time.monotonic() - stats[k]['last_cpu_times'] = p.cpu_times() - monitored_procs.append(p) - i = 0 - interval_int = int(PRINT_INTERVAL / SLEEP_INTERVAL) - while True: - for p in monitored_procs: - k = ' '.join(p.cmdline()) - cur_sys_time = time.monotonic() - cur_cpu_times = p.cpu_times() - cpu_times = np.subtract(cur_cpu_times, stats[k]['last_cpu_times']) / (cur_sys_time - stats[k]['last_sys_time']) - stats[k]['last_sys_time'] = cur_sys_time - stats[k]['last_cpu_times'] = cur_cpu_times - cpu_percent = 0 - for num, name in enumerate(cpu_time_names): - stats[k]['cpu_samples'][name].append(cpu_times[num]) - cpu_percent += cpu_times[num] - stats[k]['cpu_samples']['total'].append(cpu_percent) - time.sleep(SLEEP_INTERVAL) - i += 1 - if i % interval_int == 0: - l = [] - for k, stat in stats.items(): - if len(stat['cpu_samples']) <= 0: - continue - for name, samples in stat['cpu_samples'].items(): - samples = np.array(samples) - avg = samples.mean() - c = samples.size - min_cpu = np.amin(samples) - max_cpu = np.amax(samples) - if stat['min'][name] is None or min_cpu < stat['min'][name]: - stat['min'][name] = min_cpu - if stat['max'][name] is None or max_cpu > stat['max'][name]: - stat['max'][name] = max_cpu - stat['avg'][name] = (stat['avg'][name] * (i - c) + avg * c) / (i) - stat['cpu_samples'][name] = [] - - msg = f"avg: {stat['avg']['total']:.2%}, min: {stat['min']['total']:.2%}, max: {stat['max']['total']:.2%} {os.path.basename(k)}" - if args.detailed_times: - for stat_type in ['avg', 'min', 'max']: - msg += f"\n {stat_type}: {[(name + ':' + str(round(stat[stat_type][name] * 100, 2))) for name in cpu_time_names]}" - l.append((os.path.basename(k), stat['avg']['total'], msg)) - l.sort(key=lambda x: -x[1]) - for x in l: - print(x[2]) - print('avg sum: {:.2%} over {} samples {} seconds\n'.format( - sum(stat['avg']['total'] for k, stat in stats.items()), i, i * SLEEP_INTERVAL - )) diff --git a/selfdrive/debug/dump_car_docs.py b/selfdrive/debug/dump_car_docs.py deleted file mode 100755 index f0e99cda24..0000000000 --- a/selfdrive/debug/dump_car_docs.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import pickle - -from opendbc.car.docs import get_all_car_docs - - -def dump_car_docs(path): - with open(path, 'wb') as f: - pickle.dump(get_all_car_docs(), f) - print(f'Dumping car info to {path}') - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--path", required=True) - args = parser.parse_args() - dump_car_docs(args.path) diff --git a/selfdrive/debug/print_docs_diff.py b/selfdrive/debug/print_docs_diff.py deleted file mode 100755 index c7850939f0..0000000000 --- a/selfdrive/debug/print_docs_diff.py +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python3 -import argparse -from collections import defaultdict -import difflib -import pickle - -from opendbc.car.docs import get_all_car_docs -from opendbc.car.docs_definitions import Column - -FOOTNOTE_TAG = "{}" -STAR_ICON = '' -VIDEO_ICON = '' + \ - '' -COLUMNS = "|" + "|".join([column.value for column in Column]) + "|" -COLUMN_HEADER = "|---|---|---|{}|".format("|".join([":---:"] * (len(Column) - 3))) -ARROW_SYMBOL = "➡️" - - -def load_base_car_docs(path): - with open(path, "rb") as f: - return pickle.load(f) - - -def match_cars(base_cars, new_cars): - changes = [] - additions = [] - for new in new_cars: - # Addition if no close matches or close match already used - # Change if close match and not already used - matches = difflib.get_close_matches(new.name, [b.name for b in base_cars], cutoff=0.) - if not len(matches) or matches[0] in [c[1].name for c in changes]: - additions.append(new) - else: - changes.append((new, next(car for car in base_cars if car.name == matches[0]))) - - # Removal if base car not in changes - removals = [b for b in base_cars if b.name not in [c[1].name for c in changes]] - return changes, additions, removals - - -def build_column_diff(base_car, new_car): - row_builder = [] - for column in Column: - base_column = base_car.get_column(column, STAR_ICON, VIDEO_ICON, FOOTNOTE_TAG) - new_column = new_car.get_column(column, STAR_ICON, VIDEO_ICON, FOOTNOTE_TAG) - - if base_column != new_column: - row_builder.append(f"{base_column} {ARROW_SYMBOL} {new_column}") - else: - row_builder.append(new_column) - - return format_row(row_builder) - - -def format_row(builder): - return "|" + "|".join(builder) + "|" - - -def print_car_docs_diff(path): - base_car_docs = defaultdict(list) - new_car_docs = defaultdict(list) - - for car in load_base_car_docs(path): - base_car_docs[car.car_fingerprint].append(car) - for car in get_all_car_docs(): - new_car_docs[car.car_fingerprint].append(car) - - # Add new platforms to base cars so we can detect additions and removals in one pass - base_car_docs.update({car: [] for car in new_car_docs if car not in base_car_docs}) - - changes = defaultdict(list) - for base_car_model, base_cars in base_car_docs.items(): - # Match car info changes, and get additions and removals - new_cars = new_car_docs[base_car_model] - car_changes, car_additions, car_removals = match_cars(base_cars, new_cars) - - # Removals - for car_docs in car_removals: - changes["removals"].append(format_row([car_docs.get_column(column, STAR_ICON, VIDEO_ICON, FOOTNOTE_TAG) for column in Column])) - - # Additions - for car_docs in car_additions: - changes["additions"].append(format_row([car_docs.get_column(column, STAR_ICON, VIDEO_ICON, FOOTNOTE_TAG) for column in Column])) - - for new_car, base_car in car_changes: - # Column changes - row_diff = build_column_diff(base_car, new_car) - if ARROW_SYMBOL in row_diff: - changes["column"].append(row_diff) - - # Detail sentence changes - if base_car.detail_sentence != new_car.detail_sentence: - changes["detail"].append(f"- Sentence for {base_car.name} changed!\n" + - " ```diff\n" + - f" - {base_car.detail_sentence}\n" + - f" + {new_car.detail_sentence}\n" + - " ```") - - # Print diff - if any(len(c) for c in changes.values()): - markdown_builder = ["### ⚠️ This PR makes changes to [CARS.md](../blob/master/docs/CARS.md) ⚠️"] - - for title, category in (("## 🔀 Column Changes", "column"), ("## ❌ Removed", "removals"), - ("## ➕ Added", "additions"), ("## 📖 Detail Sentence Changes", "detail")): - if len(changes[category]): - markdown_builder.append(title) - if category not in ("detail",): - markdown_builder.append(COLUMNS) - markdown_builder.append(COLUMN_HEADER) - markdown_builder.extend(changes[category]) - - print("\n".join(markdown_builder)) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--path", required=True) - args = parser.parse_args() - print_car_docs_diff(args.path) diff --git a/selfdrive/debug/touch_replay.py b/selfdrive/debug/touch_replay.py deleted file mode 100755 index 6e5ecbbe5b..0000000000 --- a/selfdrive/debug/touch_replay.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -import argparse - -import numpy as np -import matplotlib.pyplot as plt - -from openpilot.tools.lib.logreader import LogReader - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('--width', default=2160, type=int) - parser.add_argument('--height', default=1080, type=int) - parser.add_argument('--route', default='rlog', type=str) - args = parser.parse_args() - - w = args.width - h = args.height - route = args.route - - fingers = [[-1, -1]] * 5 - touch_points = [] - current_slot = 0 - - lr = list(LogReader(route)) - for msg in lr: - if msg.which() == 'touch': - for event in msg.touch: - if event.type == 3 and event.code == 47: - current_slot = event.value - elif event.type == 3 and event.code == 57 and event.value == -1: - fingers[current_slot] = [-1, -1] - elif event.type == 3 and event.code == 53: - fingers[current_slot][1] = event.value - if fingers[current_slot][0] != -1: - touch_points.append(fingers[current_slot].copy()) - elif event.type == 3 and event.code == 54: - fingers[current_slot][0] = w - event.value - if fingers[current_slot][1] != -1: - touch_points.append(fingers[current_slot].copy()) - - if not touch_points: - print(f'No touch events found for {route}') - quit() - - unique_points, counts = np.unique(touch_points, axis=0, return_counts=True) - - plt.figure(figsize=(10, 3)) - plt.scatter(unique_points[:, 0], unique_points[:, 1], c=counts, s=counts * 20, edgecolors='red') - plt.colorbar() - plt.title(f'Touches for {route}') - plt.xlim(0, w) - plt.ylim(0, h) - plt.grid(True) - plt.show() diff --git a/selfdrive/locationd/test/test_torqued.py b/selfdrive/locationd/test/test_torqued.py deleted file mode 100644 index 53f3120c36..0000000000 --- a/selfdrive/locationd/test/test_torqued.py +++ /dev/null @@ -1,25 +0,0 @@ -from cereal import car -from openpilot.selfdrive.locationd.torqued import TorqueEstimator - - -def test_cal_percent(): - est = TorqueEstimator(car.CarParams()) - msg = est.get_msg() - assert msg.liveTorqueParameters.calPerc == 0 - - for (low, high), min_pts in zip(est.filtered_points.buckets.keys(), - est.filtered_points.buckets_min_points.values(), strict=True): - for _ in range(int(min_pts)): - est.filtered_points.add_point((low + high) / 2.0, 0.0) - - # enough bucket points, but not enough total points - msg = est.get_msg() - assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2 - - # add enough points to bucket with most capacity - key = list(est.filtered_points.buckets)[0] - for _ in range(est.min_points_total - len(est.filtered_points)): - est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0) - - msg = est.get_msg() - assert msg.liveTorqueParameters.calPerc == 100 diff --git a/selfdrive/modeld/compile_warp.py b/selfdrive/modeld/compile_warp.py deleted file mode 100755 index 1144fc69c3..0000000000 --- a/selfdrive/modeld/compile_warp.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env python3 -import time -import pickle -import numpy as np -from pathlib import Path -from tinygrad.tensor import Tensor -from tinygrad.helpers import Context -from tinygrad.device import Device -from tinygrad.engine.jit import TinyJit - -from openpilot.system.camerad.cameras.nv12_info import get_nv12_info -from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE -from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye - -MODELS_DIR = Path(__file__).parent / 'models' - -CAMERA_CONFIGS = [ - (_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208 - (_os_fisheye.width, _os_fisheye.height), # mici: 1344x760 -] - -UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32) -UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX) - -IMG_BUFFER_SHAPE = (30, MEDMODEL_INPUT_SIZE[1] // 2, MEDMODEL_INPUT_SIZE[0] // 2) - - -def warp_pkl_path(w, h): - return MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl' - - -def dm_warp_pkl_path(w, h): - return MODELS_DIR / f'dm_warp_{w}x{h}_tinygrad.pkl' - - -def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad): - w_dst, h_dst = dst_shape - h_src, w_src = src_shape - - x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1) - y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1) - - # inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather) - src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2] - src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2] - src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2] - - src_x = src_x / src_w - src_y = src_y / src_w - - x_nn_clipped = Tensor.round(src_x).clip(0, w_src - 1).cast('int') - y_nn_clipped = Tensor.round(src_y).clip(0, h_src - 1).cast('int') - idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped - - return src_flat[idx] - - -def frames_to_tensor(frames, model_w, model_h): - H = (frames.shape[0] * 2) // 3 - W = frames.shape[1] - in_img1 = Tensor.cat(frames[0:H:2, 0::2], - frames[1:H:2, 0::2], - frames[0:H:2, 1::2], - frames[1:H:2, 1::2], - frames[H:H+H//4].reshape((H//2, W//2)), - frames[H+H//4:H+H//2].reshape((H//2, W//2)), dim=0).reshape((6, H//2, W//2)) - return in_img1 - - -def make_frame_prepare(cam_w, cam_h, model_w, model_h): - stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h) - uv_offset = stride * y_height - stride_pad = stride - cam_w - - def frame_prepare_tinygrad(input_frame, M_inv): - # UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling - M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]]) - # deinterleave NV12 UV plane (UVUV... -> separate U, V) - uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride) - with Context(SPLIT_REDUCEOP=0): - y = warp_perspective_tinygrad(input_frame[:cam_h*stride], - M_inv, (model_w, model_h), - (cam_h, cam_w), stride_pad).realize() - u = warp_perspective_tinygrad(uv[:cam_h//2, :cam_w:2].flatten(), - M_inv_uv, (model_w//2, model_h//2), - (cam_h//2, cam_w//2), 0).realize() - v = warp_perspective_tinygrad(uv[:cam_h//2, 1:cam_w:2].flatten(), - M_inv_uv, (model_w//2, model_h//2), - (cam_h//2, cam_w//2), 0).realize() - yuv = y.cat(u).cat(v).reshape((model_h * 3 // 2, model_w)) - tensor = frames_to_tensor(yuv, model_w, model_h) - return tensor - return frame_prepare_tinygrad - - -def make_update_img_input(frame_prepare, model_w, model_h): - def update_img_input_tinygrad(tensor, frame, M_inv): - M_inv = M_inv.to(Device.DEFAULT) - new_img = frame_prepare(frame, M_inv) - tensor.assign(tensor[6:].cat(new_img, dim=0).contiguous()) - return Tensor.cat(tensor[:6], tensor[-6:], dim=0).contiguous().reshape(1, 12, model_h//2, model_w//2) - return update_img_input_tinygrad - - -def make_update_both_imgs(frame_prepare, model_w, model_h): - update_img = make_update_img_input(frame_prepare, model_w, model_h) - - def update_both_imgs_tinygrad(calib_img_buffer, new_img, M_inv, - calib_big_img_buffer, new_big_img, M_inv_big): - calib_img_pair = update_img(calib_img_buffer, new_img, M_inv) - calib_big_img_pair = update_img(calib_big_img_buffer, new_big_img, M_inv_big) - return calib_img_pair, calib_big_img_pair - return update_both_imgs_tinygrad - - -def make_warp_dm(cam_w, cam_h, dm_w, dm_h): - stride, y_height, _, _ = get_nv12_info(cam_w, cam_h) - stride_pad = stride - cam_w - - def warp_dm(input_frame, M_inv): - M_inv = M_inv.to(Device.DEFAULT) - result = warp_perspective_tinygrad(input_frame[:cam_h*stride], M_inv, (dm_w, dm_h), (cam_h, cam_w), stride_pad).reshape(-1, dm_h * dm_w) - return result - return warp_dm - - -def compile_modeld_warp(cam_w, cam_h): - model_w, model_h = MEDMODEL_INPUT_SIZE - _, _, _, yuv_size = get_nv12_info(cam_w, cam_h) - - print(f"Compiling modeld warp for {cam_w}x{cam_h}...") - - frame_prepare = make_frame_prepare(cam_w, cam_h, model_w, model_h) - update_both_imgs = make_update_both_imgs(frame_prepare, model_w, model_h) - update_img_jit = TinyJit(update_both_imgs, prune=True) - - full_buffer = Tensor.zeros(IMG_BUFFER_SHAPE, dtype='uint8').contiguous().realize() - big_full_buffer = Tensor.zeros(IMG_BUFFER_SHAPE, dtype='uint8').contiguous().realize() - new_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8) - new_big_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8) - for i in range(10): - img_inputs = [full_buffer, - Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(), - Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')] - big_img_inputs = [big_full_buffer, - Tensor.from_blob(new_big_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(), - Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')] - inputs = img_inputs + big_img_inputs - Device.default.synchronize() - - st = time.perf_counter() - _ = update_img_jit(*inputs) - mt = time.perf_counter() - Device.default.synchronize() - et = time.perf_counter() - print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms") - - pkl_path = warp_pkl_path(cam_w, cam_h) - with open(pkl_path, "wb") as f: - pickle.dump(update_img_jit, f) - print(f" Saved to {pkl_path}") - - jit = pickle.load(open(pkl_path, "rb")) - jit(*inputs) - - -def compile_dm_warp(cam_w, cam_h): - dm_w, dm_h = DM_INPUT_SIZE - _, _, _, yuv_size = get_nv12_info(cam_w, cam_h) - - print(f"Compiling DM warp for {cam_w}x{cam_h}...") - - warp_dm = make_warp_dm(cam_w, cam_h, dm_w, dm_h) - warp_dm_jit = TinyJit(warp_dm, prune=True) - - new_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8) - for i in range(10): - inputs = [Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(), - Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')] - Device.default.synchronize() - st = time.perf_counter() - warp_dm_jit(*inputs) - mt = time.perf_counter() - Device.default.synchronize() - et = time.perf_counter() - print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms") - - pkl_path = dm_warp_pkl_path(cam_w, cam_h) - with open(pkl_path, "wb") as f: - pickle.dump(warp_dm_jit, f) - print(f" Saved to {pkl_path}") - - -def run_and_save_pickle(): - for cam_w, cam_h in CAMERA_CONFIGS: - compile_modeld_warp(cam_w, cam_h) - compile_dm_warp(cam_w, cam_h) - - -if __name__ == "__main__": - run_and_save_pickle() diff --git a/selfdrive/modeld/helpers.py b/selfdrive/modeld/helpers.py deleted file mode 100644 index 64bf28873f..0000000000 --- a/selfdrive/modeld/helpers.py +++ /dev/null @@ -1,26 +0,0 @@ -import json -from pathlib import Path - -MODELS_DIR = Path(__file__).resolve().parent / 'models' -TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json' -USBGPU_VID = 0xADD1 -USBGPU_PID = 0x0001 - - -def get_tg_input_devices(process_name: str, usbgpu: bool): - with open(TG_INPUT_DEVICES_PATH) as f: - return json.load(f)[process_name]['default' if not usbgpu else 'usbgpu'] - -def modeld_pkl_path(usbgpu: bool): - prefix = 'big_' if usbgpu else '' - return MODELS_DIR / f'{prefix}driving_tinygrad.pkl' - -def usbgpu_present() -> bool: - for d in Path("/sys/bus/usb/devices").glob("*"): - try: - if int((d / "idVendor").read_text(), 16) == USBGPU_VID and \ - int((d / "idProduct").read_text(), 16) == USBGPU_PID: - return True - except Exception: - pass - return False diff --git a/selfdrive/modeld/models/README.md b/selfdrive/modeld/models/README.md deleted file mode 100644 index 04b69c61c3..0000000000 --- a/selfdrive/modeld/models/README.md +++ /dev/null @@ -1,66 +0,0 @@ -## Neural networks in openpilot -To view the architecture of the ONNX networks, you can use [netron](https://netron.app/) - -## Driving Model (vision model + temporal policy model) -### Vision inputs (Full size: 799906 x float32) -* **image stream** - * Two consecutive images (256 * 512 * 3 in RGB) recorded at 20 Hz : 393216 = 2 * 6 * 128 * 256 - * Each 256 * 512 image is represented in YUV420 with 6 channels : 6 * 128 * 256 - * Channels 0,1,2,3 represent the full-res Y channel and are represented in numpy as Y[::2, ::2], Y[::2, 1::2], Y[1::2, ::2], and Y[1::2, 1::2] - * Channel 4 represents the half-res U channel - * Channel 5 represents the half-res V channel -* **wide image stream** - * Two consecutive images (256 * 512 * 3 in RGB) recorded at 20 Hz : 393216 = 2 * 6 * 128 * 256 - * Each 256 * 512 image is represented in YUV420 with 6 channels : 6 * 128 * 256 - * Channels 0,1,2,3 represent the full-res Y channel and are represented in numpy as Y[::2, ::2], Y[::2, 1::2], Y[1::2, ::2], and Y[1::2, 1::2] - * Channel 4 represents the half-res U channel - * Channel 5 represents the half-res V channel -### Policy inputs -* **desire** - * one-hot encoded buffer to command model to execute certain actions, bit needs to be sent for the past 5 seconds (at 20FPS) : 100 * 8 -* **traffic convention** - * one-hot encoded vector to tell model whether traffic is right-hand or left-hand traffic : 2 -* **lateral control params** - * speed and steering delay for predicting the desired curvature: 2 -* **previous desired curvatures** - * vector of previously predicted desired curvatures: 100 * 1 -* **feature buffer** - * a buffer of intermediate features including the current feature to form a 5 seconds temporal context (at 20FPS) : 100 * 512 - - -### Driving Model output format (Full size: XXX x float32) -Refer to **slice_outputs** and **parse_vision_outputs/parse_policy_outputs** in modeld. - - -## Driver Monitoring Model -* .onnx model can be run with onnx runtimes -* .dlc file is a pre-quantized model and only runs on qualcomm DSPs - -### input format -* single image W = 1440 H = 960 luminance channel (Y) from the planar YUV420 format: - * full input size is 1440 * 960 = 1382400 - * normalized ranging from 0.0 to 1.0 in float32 (onnx runner) or ranging from 0 to 255 in uint8 (snpe runner) -* camera calibration angles (roll, pitch, yaw) from liveCalibration: 3 x float32 inputs - -### output format -* 84 x float32 outputs = 2 + 41 * 2 ([parsing example](https://github.com/commaai/openpilot/blob/22ce4e17ba0d3bfcf37f8255a4dd1dc683fe0c38/selfdrive/modeld/models/dmonitoring.cc#L33)) - * for each person in the front seats (2 * 41) - * face pose: 12 = 6 + 6 - * face orientation [pitch, yaw, roll] in camera frame: 3 - * face position [dx, dy] relative to image center: 2 - * normalized face size: 1 - * standard deviations for above outputs: 6 - * face visible probability: 1 - * eyes: 20 = (8 + 1) + (8 + 1) + 1 + 1 - * eye position and size, and their standard deviations: 8 - * eye visible probability: 1 - * eye closed probability: 1 - * wearing sunglasses probability: 1 - * face occluded probability: 1 - * touching wheel probability: 1 - * paying attention probability: 1 - * (deprecated) distracted probabilities: 2 - * using phone probability: 1 - * distracted probability: 1 - * common outputs 1 - * left hand drive probability: 1 diff --git a/selfdrive/modeld/models/big_driving_on_policy.onnx b/selfdrive/modeld/models/big_driving_on_policy.onnx deleted file mode 100644 index f7b49c018a..0000000000 --- a/selfdrive/modeld/models/big_driving_on_policy.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:565e53c38dcd64c50dd3fe4d5ee1530213aeefd66c3f6b67ea6a72a32612a6bf -size 14061419 diff --git a/selfdrive/modeld/models/big_driving_vision.onnx b/selfdrive/modeld/models/big_driving_vision.onnx deleted file mode 100644 index d14f1969e0..0000000000 --- a/selfdrive/modeld/models/big_driving_vision.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1f0cab5033fe9e3bc5e174a2e790fa277f7d9fc44c65822d734064d2f899a9a0 -size 296203378 diff --git a/selfdrive/modeld/models/driving_on_policy.onnx b/selfdrive/modeld/models/driving_on_policy.onnx deleted file mode 100644 index 611ae9fe85..0000000000 --- a/selfdrive/modeld/models/driving_on_policy.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:78477124cbf3ffe30fa951ebada8410b43c4242c6054584d656f1d329b067e15 -size 14060847 diff --git a/selfdrive/modeld/models/driving_vision.onnx b/selfdrive/modeld/models/driving_vision.onnx deleted file mode 100644 index 6c9fc4c84d..0000000000 --- a/selfdrive/modeld/models/driving_vision.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ee29ee5bce84d1ce23e9ff381280de9b4e4d96d2934cd751740354884e112c66 -size 46877473 diff --git a/selfdrive/test/docker_build.sh b/selfdrive/test/docker_build.sh deleted file mode 100755 index 8d1fa82249..0000000000 --- a/selfdrive/test/docker_build.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -e - -SCRIPT_DIR=$(dirname "$0") -OPENPILOT_DIR=$SCRIPT_DIR/../../ - -DOCKER_IMAGE=openpilot -DOCKER_FILE=Dockerfile.openpilot -DOCKER_REGISTRY=ghcr.io/commaai -COMMIT_SHA=$(git rev-parse HEAD) - -if [ -n "$TARGET_ARCHITECTURE" ]; then - PLATFORM="linux/$TARGET_ARCHITECTURE" - TAG_SUFFIX="-$TARGET_ARCHITECTURE" -else - PLATFORM="linux/$(uname -m)" - TAG_SUFFIX="" -fi - -LOCAL_TAG=$DOCKER_IMAGE$TAG_SUFFIX -REMOTE_TAG=$DOCKER_REGISTRY/$LOCAL_TAG -REMOTE_SHA_TAG=$DOCKER_REGISTRY/$LOCAL_TAG:$COMMIT_SHA - -DOCKER_BUILDKIT=1 docker buildx build --provenance false --pull --platform $PLATFORM --load -t $DOCKER_IMAGE:latest -t $REMOTE_TAG -t $LOCAL_TAG -f $OPENPILOT_DIR/$DOCKER_FILE $OPENPILOT_DIR - -if [ -n "$PUSH_IMAGE" ]; then - docker push $REMOTE_TAG - docker tag $REMOTE_TAG $REMOTE_SHA_TAG - docker push $REMOTE_SHA_TAG -fi diff --git a/selfdrive/test/setup_xvfb.sh b/selfdrive/test/setup_xvfb.sh deleted file mode 100755 index 50b313658d..0000000000 --- a/selfdrive/test/setup_xvfb.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -# Sets up a virtual display for running map renderer and simulator without an X11 display - -if uname -r | grep -q "WSL2"; then - DISP_ID=0 # WSLg uses display :0 -else - DISP_ID=99 # Standard Xvfb display -fi -export DISPLAY=:$DISP_ID - -Xvfb $DISPLAY -screen 0 2160x1080x24 2>/dev/null & - -# check for x11 socket for the specified display ID -while [ ! -S /tmp/.X11-unix/X$DISP_ID ] -do - echo "Waiting for Xvfb..." - sleep 1 -done - -touch ~/.Xauthority -export XDG_SESSION_TYPE="x11" \ No newline at end of file diff --git a/selfdrive/test/test_updated.py b/selfdrive/test/test_updated.py deleted file mode 100644 index e4cc714b87..0000000000 --- a/selfdrive/test/test_updated.py +++ /dev/null @@ -1,295 +0,0 @@ -import datetime -import os -import pytest -import time -import tempfile -import shutil -import signal -import subprocess -import random - -from openpilot.common.basedir import BASEDIR -from openpilot.common.params import Params - - -@pytest.mark.tici -class TestUpdated: - - def setup_method(self): - self.updated_proc = None - - self.tmp_dir = tempfile.TemporaryDirectory() - org_dir = os.path.join(self.tmp_dir.name, "commaai") - - self.basedir = os.path.join(org_dir, "openpilot") - self.git_remote_dir = os.path.join(org_dir, "openpilot_remote") - self.staging_dir = os.path.join(org_dir, "safe_staging") - for d in [org_dir, self.basedir, self.git_remote_dir, self.staging_dir]: - os.mkdir(d) - - self.neos_version = os.path.join(org_dir, "neos_version") - self.neosupdate_dir = os.path.join(org_dir, "neosupdate") - with open(self.neos_version, "w") as f: - v = subprocess.check_output(r"bash -c 'source launch_env.sh && echo $REQUIRED_NEOS_VERSION'", - cwd=BASEDIR, shell=True, encoding='utf8').strip() - f.write(v) - - self.upper_dir = os.path.join(self.staging_dir, "upper") - self.merged_dir = os.path.join(self.staging_dir, "merged") - self.finalized_dir = os.path.join(self.staging_dir, "finalized") - - # setup local submodule remotes - submodules = subprocess.check_output("git submodule --quiet foreach 'echo $name'", - shell=True, cwd=BASEDIR, encoding='utf8').split() - for s in submodules: - sub_path = os.path.join(org_dir, s.split("_repo")[0]) - self._run(f"git clone {s} {sub_path}.git", cwd=BASEDIR) - - # setup two git repos, a remote and one we'll run updated in - self._run([ - f"git clone {BASEDIR} {self.git_remote_dir}", - f"git clone {self.git_remote_dir} {self.basedir}", - f"cd {self.basedir} && git submodule init && git submodule update", - f"cd {self.basedir} && scons cereal/ common/" - ]) - - self.params = Params(os.path.join(self.basedir, "persist/params")) - self.params.clear_all() - os.sync() - - def teardown_method(self): - try: - if self.updated_proc is not None: - self.updated_proc.terminate() - self.updated_proc.wait(30) - except Exception as e: - print(e) - self.tmp_dir.cleanup() - - - # *** test helpers *** - - - def _run(self, cmd, cwd=None): - if not isinstance(cmd, list): - cmd = (cmd,) - - for c in cmd: - subprocess.check_output(c, cwd=cwd, shell=True) - - def _get_updated_proc(self): - os.environ["PYTHONPATH"] = self.basedir - os.environ["GIT_AUTHOR_NAME"] = "testy tester" - os.environ["GIT_COMMITTER_NAME"] = "testy tester" - os.environ["GIT_AUTHOR_EMAIL"] = "testy@tester.test" - os.environ["GIT_COMMITTER_EMAIL"] = "testy@tester.test" - os.environ["UPDATER_TEST_IP"] = "localhost" - os.environ["UPDATER_LOCK_FILE"] = os.path.join(self.tmp_dir.name, "updater.lock") - os.environ["UPDATER_STAGING_ROOT"] = self.staging_dir - os.environ["UPDATER_NEOS_VERSION"] = self.neos_version - os.environ["UPDATER_NEOSUPDATE_DIR"] = self.neosupdate_dir - updated_path = os.path.join(self.basedir, "system/updated.py") - return subprocess.Popen(updated_path, env=os.environ) - - def _start_updater(self, offroad=True, nosleep=False): - self.params.put_bool("IsOffroad", offroad, block=True) - self.updated_proc = self._get_updated_proc() - if not nosleep: - time.sleep(1) - - def _update_now(self): - self.updated_proc.send_signal(signal.SIGHUP) - - # TODO: this should be implemented in params - def _read_param(self, key, timeout=1): - ret = None - start_time = time.monotonic() - while ret is None: - ret = self.params.get(key) - if time.monotonic() - start_time > timeout: - break - time.sleep(0.01) - return ret - - def _wait_for_update(self, timeout=30, clear_param=False): - if clear_param: - self.params.remove("LastUpdateTime") - - self._update_now() - t = self._read_param("LastUpdateTime", timeout=timeout) - if t is None: - raise Exception("timed out waiting for update to complete") - - def _make_commit(self): - all_dirs, all_files = [], [] - for root, dirs, files in os.walk(self.git_remote_dir): - if ".git" in root: - continue - for d in dirs: - all_dirs.append(os.path.join(root, d)) - for f in files: - all_files.append(os.path.join(root, f)) - - # make a new dir and some new files - new_dir = os.path.join(self.git_remote_dir, "this_is_a_new_dir") - os.mkdir(new_dir) - for _ in range(random.randrange(5, 30)): - for d in (new_dir, random.choice(all_dirs)): - with tempfile.NamedTemporaryFile(dir=d, delete=False) as f: - f.write(os.urandom(random.randrange(1, 1000000))) - - # modify some files - for f in random.sample(all_files, random.randrange(5, 50)): - with open(f, "w+") as ff: - txt = ff.readlines() - ff.seek(0) - for line in txt: - ff.write(line[::-1]) - - # remove some files - for f in random.sample(all_files, random.randrange(5, 50)): - os.remove(f) - - # remove some dirs - for d in random.sample(all_dirs, random.randrange(1, 10)): - shutil.rmtree(d) - - # commit the changes - self._run([ - "git add -A", - "git commit -m 'an update'", - ], cwd=self.git_remote_dir) - - def _check_update_state(self, update_available): - # make sure LastUpdateTime is recent - last_update_time = self._read_param("LastUpdateTime") - td = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - last_update_time - assert td.total_seconds() < 10 - self.params.remove("LastUpdateTime") - - # wait a bit for the rest of the params to be written - time.sleep(0.1) - - # check params - update = self._read_param("UpdateAvailable") - assert update == "1" == update_available, f"UpdateAvailable: {repr(update)}" - assert self._read_param("UpdateFailedCount") == 0 - - # TODO: check that the finalized update actually matches remote - # check the .overlay_init and .overlay_consistent flags - assert os.path.isfile(os.path.join(self.basedir, ".overlay_init")) - assert os.path.isfile(os.path.join(self.finalized_dir, ".overlay_consistent")) == update_available - - - # *** test cases *** - - - # Run updated for 100 cycles with no update - def test_no_update(self): - self._start_updater() - for _ in range(100): - self._wait_for_update(clear_param=True) - self._check_update_state(False) - - # Let the updater run with no update for a cycle, then write an update - def test_update(self): - self._start_updater() - - # run for a cycle with no update - self._wait_for_update(clear_param=True) - self._check_update_state(False) - - # write an update to our remote - self._make_commit() - - # run for a cycle to get the update - self._wait_for_update(timeout=60, clear_param=True) - self._check_update_state(True) - - # run another cycle with no update - self._wait_for_update(clear_param=True) - self._check_update_state(True) - - # Let the updater run for 10 cycles, and write an update every cycle - @pytest.mark.skip("need to make this faster") - def test_update_loop(self): - self._start_updater() - - # run for a cycle with no update - self._wait_for_update(clear_param=True) - for _ in range(10): - time.sleep(0.5) - self._make_commit() - self._wait_for_update(timeout=90, clear_param=True) - self._check_update_state(True) - - # Test overlay re-creation after tracking a new file in basedir's git - def test_overlay_reinit(self): - self._start_updater() - - overlay_init_fn = os.path.join(self.basedir, ".overlay_init") - - # run for a cycle with no update - self._wait_for_update(clear_param=True) - self.params.remove("LastUpdateTime") - first_mtime = os.path.getmtime(overlay_init_fn) - - # touch a file in the basedir - self._run("touch new_file && git add new_file", cwd=self.basedir) - - # run another cycle, should have a new mtime - self._wait_for_update(clear_param=True) - second_mtime = os.path.getmtime(overlay_init_fn) - assert first_mtime != second_mtime - - # run another cycle, mtime should be same as last cycle - self._wait_for_update(clear_param=True) - new_mtime = os.path.getmtime(overlay_init_fn) - assert second_mtime == new_mtime - - # Make sure updated exits if another instance is running - def test_multiple_instances(self): - # start updated and let it run for a cycle - self._start_updater() - time.sleep(1) - self._wait_for_update(clear_param=True) - - # start another instance - second_updated = self._get_updated_proc() - ret_code = second_updated.wait(timeout=5) - assert ret_code is not None - - - # *** test cases with NEOS updates *** - - - # Run updated with no update, make sure it clears the old NEOS update - def test_clear_neos_cache(self): - # make the dir and some junk files - os.mkdir(self.neosupdate_dir) - for _ in range(15): - with tempfile.NamedTemporaryFile(dir=self.neosupdate_dir, delete=False) as f: - f.write(os.urandom(random.randrange(1, 1000000))) - - self._start_updater() - self._wait_for_update(clear_param=True) - self._check_update_state(False) - assert not os.path.isdir(self.neosupdate_dir) - - # Let the updater run with no update for a cycle, then write an update - @pytest.mark.skip("TODO: only runs on device") - def test_update_with_neos_update(self): - # bump the NEOS version and commit it - self._run([ - "echo 'export REQUIRED_NEOS_VERSION=3' >> launch_env.sh", - "git -c user.name='testy' -c user.email='testy@tester.test' \ - commit -am 'a neos update'", - ], cwd=self.git_remote_dir) - - # run for a cycle to get the update - self._start_updater() - self._wait_for_update(timeout=60, clear_param=True) - self._check_update_state(True) - - # TODO: more comprehensive check - assert os.path.isdir(self.neosupdate_dir) diff --git a/selfdrive/ui/mici/tests/test_widget_leaks.py b/selfdrive/ui/mici/tests/test_widget_leaks.py deleted file mode 100755 index e35cb44776..0000000000 --- a/selfdrive/ui/mici/tests/test_widget_leaks.py +++ /dev/null @@ -1,119 +0,0 @@ -import pyray as rl -rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN) -import gc -import weakref -import pytest -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.widgets import Widget - -# mici dialogs -from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow -from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog -from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog -from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog -from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal - -# tici dialogs -from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog -from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow -from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog -from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog -from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog -from openpilot.system.ui.widgets.html_render import HtmlModal -from openpilot.system.ui.widgets.keyboard import Keyboard - -# FIXME: known small leaks not worth worrying about at the moment -KNOWN_LEAKS = { - "openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog.DriverCameraView", - "openpilot.selfdrive.ui.mici.layouts.onboarding.TermsPage", - "openpilot.selfdrive.ui.mici.layouts.onboarding.TrainingGuide", - "openpilot.selfdrive.ui.mici.layouts.onboarding.DeclinePage", - "openpilot.selfdrive.ui.mici.layouts.onboarding.OnboardingWindow", - "openpilot.selfdrive.ui.onroad.driver_state.DriverStateRenderer", - "openpilot.selfdrive.ui.onroad.driver_camera_dialog.DriverCameraDialog", - "openpilot.selfdrive.ui.layouts.onboarding.TermsPage", - "openpilot.selfdrive.ui.layouts.onboarding.DeclinePage", - "openpilot.selfdrive.ui.layouts.onboarding.OnboardingWindow", - "openpilot.system.ui.widgets.confirm_dialog.ConfirmDialog", - "openpilot.system.ui.widgets.label.Label", - "openpilot.system.ui.widgets.button.Button", - "openpilot.system.ui.widgets.html_render.HtmlRenderer", - "openpilot.system.ui.widgets.nav_widget.NavBar", - "openpilot.selfdrive.ui.mici.layouts.settings.device.MiciFccModal", - "openpilot.system.ui.widgets.inputbox.InputBox", - "openpilot.system.ui.widgets.scroller_tici.Scroller", - "openpilot.system.ui.widgets.label.UnifiedLabel", - "openpilot.system.ui.widgets.mici_keyboard.MiciKeyboard", - "openpilot.selfdrive.ui.mici.widgets.dialog.BigConfirmationDialog", - "openpilot.system.ui.widgets.keyboard.Keyboard", - "openpilot.system.ui.widgets.slider.BigSlider", - "openpilot.selfdrive.ui.mici.widgets.dialog.BigInputDialog", - "openpilot.system.ui.widgets.option_dialog.MultiOptionDialog", -} - - -def get_child_widgets(widget: Widget) -> list[Widget]: - children = [] - for val in widget.__dict__.values(): - items = val if isinstance(val, (list, tuple)) else (val,) - children.extend(w for w in items if isinstance(w, Widget)) - return children - - -@pytest.mark.skip(reason="segfaults") -def test_dialogs_do_not_leak(): - gui_app.init_window("ref-test") - - leaked_widgets = set() - - for ctor in ( - # mici - MiciDriverCameraDialog, MiciPairingDialog, - lambda: MiciTrainingGuide(lambda: None), - lambda: MiciOnboardingWindow(lambda: None), - lambda: BigDialog("test", "test"), - lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None), - lambda: BigInputDialog("test"), - lambda: MiciFccModal(text="test"), - # tici - TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard, - lambda: ConfirmDialog("test", "ok"), - lambda: MultiOptionDialog("test", ["a", "b"]), - lambda: HtmlModal(text="test"), - ): - widget = ctor() - all_refs = [weakref.ref(w) for w in get_child_widgets(widget) + [widget]] - - del widget - - for ref in all_refs: - if ref() is not None: - obj = ref() - name = f"{type(obj).__module__}.{type(obj).__qualname__}" - leaked_widgets.add(name) - - print(f"\n=== Widget {name} alive after del") - print(" Referrers:") - for r in gc.get_referrers(obj): - if r is obj: - continue - - if hasattr(r, '__self__') and r.__self__ is not obj: - print(f" bound method: {type(r.__self__).__qualname__}.{r.__name__}") - elif hasattr(r, '__func__'): - print(f" method: {r.__name__}") - else: - print(f" {type(r).__module__}.{type(r).__qualname__}") - del obj - - gui_app.close() - - unexpected = leaked_widgets - KNOWN_LEAKS - assert not unexpected, f"New leaked widgets: {unexpected}" - - fixed = KNOWN_LEAKS - leaked_widgets - assert not fixed, f"These leaks are fixed, remove from KNOWN_LEAKS: {fixed}" - - -if __name__ == "__main__": - test_dialogs_do_not_leak() diff --git a/selfdrive/ui/tests/diff/diff.py b/selfdrive/ui/tests/diff/diff.py deleted file mode 100755 index 974edb42a3..0000000000 --- a/selfdrive/ui/tests/diff/diff.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python3 -import os -import sys -import subprocess -import webbrowser -import argparse -from pathlib import Path -from openpilot.common.basedir import BASEDIR - -DIFF_OUT_DIR = Path(BASEDIR) / "selfdrive" / "ui" / "tests" / "diff" / "report" -HTML_TEMPLATE_PATH = Path(__file__).with_name("diff_template.html") - - -def extract_framehashes(video_path): - cmd = ['ffmpeg', '-i', video_path, '-map', '0:v:0', '-vsync', '0', '-f', 'framehash', '-hash', 'md5', '-'] - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - hashes = [] - for line in result.stdout.splitlines(): - if not line or line.startswith('#'): - continue - parts = line.split(',') - if len(parts) < 4: - continue - hashes.append(parts[-1].strip()) - return hashes - - -def create_diff_video(video1, video2, output_path): - """Create a diff video using ffmpeg blend filter with difference mode.""" - print("Creating diff video...") - cmd = ['ffmpeg', '-i', video1, '-i', video2, '-filter_complex', '[0:v]blend=all_mode=difference', '-vsync', '0', '-y', output_path] - subprocess.run(cmd, capture_output=True, check=True) - - -def find_differences(video1, video2) -> tuple[list[int], tuple[int, int]]: - print(f"Hashing frames from {video1}...") - hashes1 = extract_framehashes(video1) - - print(f"Hashing frames from {video2}...") - hashes2 = extract_framehashes(video2) - - print(f"Comparing {len(hashes1)} frames...") - different_frames = [] - - for i, (h1, h2) in enumerate(zip(hashes1, hashes2, strict=False)): - if h1 != h2: - different_frames.append(i) - - return different_frames, (len(hashes1), len(hashes2)) - - -def generate_html_report(videos: tuple[str, str], basedir: str, different_frames: list[int], frame_counts: tuple[int, int], diff_video_name): - chunks = [] - if different_frames: - current_chunk = [different_frames[0]] - for i in range(1, len(different_frames)): - if different_frames[i] == different_frames[i - 1] + 1: - current_chunk.append(different_frames[i]) - else: - chunks.append(current_chunk) - current_chunk = [different_frames[i]] - chunks.append(current_chunk) - - total_frames = max(frame_counts) - frame_delta = frame_counts[1] - frame_counts[0] - different_total = len(different_frames) + abs(frame_delta) - - result_text = ( - f"✅ Videos are identical! ({total_frames} frames)" - if different_total == 0 - else f"❌ Found {different_total} different frames out of {total_frames} total ({different_total / total_frames * 100:.1f}%)." - + (f" Video {'2' if frame_delta > 0 else '1'} is longer by {abs(frame_delta)} frames." if frame_delta != 0 else "") - ) - - # Load HTML template and replace placeholders - html = HTML_TEMPLATE_PATH.read_text() - placeholders = { - "VIDEO1_SRC": os.path.join(basedir, os.path.basename(videos[0])), - "VIDEO2_SRC": os.path.join(basedir, os.path.basename(videos[1])), - "DIFF_SRC": os.path.join(basedir, diff_video_name), - "RESULT_TEXT": result_text, - } - for key, value in placeholders.items(): - html = html.replace(f"${key}", value) - - return html - - -def main(): - parser = argparse.ArgumentParser(description='Compare two videos and generate HTML diff report') - parser.add_argument('video1', help='First video file') - parser.add_argument('video2', help='Second video file') - parser.add_argument('output', nargs='?', default='diff.html', help='Output HTML file (default: diff.html)') - parser.add_argument("--basedir", type=str, help="Base directory for output", default="") - parser.add_argument('--no-open', action='store_true', help='Do not open HTML report in browser') - - args = parser.parse_args() - - if not args.output.lower().endswith('.html'): - args.output += '.html' - - os.makedirs(DIFF_OUT_DIR, exist_ok=True) - - print("=" * 60) - print("VIDEO DIFF - HTML REPORT") - print("=" * 60) - print(f"Video 1: {args.video1}") - print(f"Video 2: {args.video2}") - print(f"Output: {args.output}") - print() - - # Create diff video with name derived from output HTML - diff_video_name = Path(args.output).stem + '.mp4' - diff_video_path = str(DIFF_OUT_DIR / diff_video_name) - create_diff_video(args.video1, args.video2, diff_video_path) - - different_frames, frame_counts = 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_counts, diff_video_name) - - with open(DIFF_OUT_DIR / args.output, 'w') as f: - f.write(html) - - # Open in browser by default - if not args.no_open: - print(f"Opening {args.output} in browser...") - webbrowser.open(f'file://{os.path.abspath(DIFF_OUT_DIR / args.output)}') - - extra_frames = abs(frame_counts[0] - frame_counts[1]) - return 0 if (len(different_frames) + extra_frames) == 0 else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/selfdrive/ui/tests/test_raylib_ui.py b/selfdrive/ui/tests/test_raylib_ui.py deleted file mode 100644 index 69ba946dcd..0000000000 --- a/selfdrive/ui/tests/test_raylib_ui.py +++ /dev/null @@ -1,8 +0,0 @@ -import time -from openpilot.selfdrive.test.helpers import with_processes - - -@with_processes(["ui"]) -def test_raylib_ui(): - """Test initialization of the UI widgets is successful.""" - time.sleep(1) diff --git a/selfdrive/ui/tests/test_translations.py b/selfdrive/ui/tests/test_translations.py deleted file mode 100644 index fba595acad..0000000000 --- a/selfdrive/ui/tests/test_translations.py +++ /dev/null @@ -1,106 +0,0 @@ -import json -import re -import string -from pathlib import Path - -import pytest - -from openpilot.selfdrive.ui.translations.potools import parse_po -from openpilot.system.ui.lib.multilang import LANGUAGES_FILE, TRANSLATIONS_DIR - -PERCENT_PLACEHOLDER_RE = re.compile(r"%(?:n|\d+)") -BAD_ENTITY_RE = re.compile(r'@(\w+);') -LINE_NUMBER_REF_RE = re.compile(r'^#:\s+.+:\d+(?:\s|$)') -FORMATTER = string.Formatter() -PO_DIR = Path(str(TRANSLATIONS_DIR)) - -with LANGUAGES_FILE.open(encoding='utf-8') as f: - TRANSLATION_LANGUAGES = json.load(f) - - -def extract_placeholders(text: str) -> list[str]: - placeholders = PERCENT_PLACEHOLDER_RE.findall(text) - - try: - parsed = list(FORMATTER.parse(text)) - except ValueError as e: - raise AssertionError(f"invalid brace formatting in {text!r}: {e}") from e - - for _, field_name, format_spec, conversion in parsed: - if field_name is None: - continue - - token = "{" - token += field_name - if conversion: - token += f"!{conversion}" - if format_spec: - token += f":{format_spec}" - token += "}" - placeholders.append(token) - - return sorted(placeholders) - - -def load_po_text(po_path: Path) -> str: - return po_path.read_text(encoding='utf-8') - - -@pytest.mark.parametrize("language_code", sorted(TRANSLATION_LANGUAGES.values())) -def test_translation_file_exists(language_code: str): - po_path = PO_DIR / f"app_{language_code}.po" - assert po_path.exists(), f"missing translation file: {po_path}" - - -@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) -def test_translation_placeholders_are_preserved(po_path: Path): - _, entries = parse_po(po_path) - language = po_path.stem.removeprefix("app_") - - for entry in entries: - source_placeholders = extract_placeholders(entry.msgid) - - if entry.is_plural: - plural_placeholders = extract_placeholders(entry.msgid_plural) - message = ( - f"{language}: source plural placeholders do not match singular for " - + f"{entry.msgid!r}: {source_placeholders} vs {plural_placeholders}" - ) - assert plural_placeholders == source_placeholders, message - - for idx, msgstr in sorted(entry.msgstr_plural.items()): - if not msgstr: - continue - - translated_placeholders = extract_placeholders(msgstr) - message = ( - f"{language}: plural form {idx} changes placeholders for {entry.msgid!r}: " - + f"expected {source_placeholders}, got {translated_placeholders}" - ) - assert translated_placeholders == source_placeholders, message - else: - if not entry.msgstr: - continue - - translated_placeholders = extract_placeholders(entry.msgstr) - message = ( - f"{language}: translation changes placeholders for {entry.msgid!r}: " - + f"expected {source_placeholders}, got {translated_placeholders}" - ) - assert translated_placeholders == source_placeholders, message - - -@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) -def test_translation_refs_do_not_include_line_numbers(po_path: Path): - for line in load_po_text(po_path).splitlines(): - assert not LINE_NUMBER_REF_RE.match(line), ( - f"{po_path.name}: line-number source reference found: {line}" - ) - - -@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) -def test_translation_entities_are_valid(po_path: Path): - matches = BAD_ENTITY_RE.findall(load_po_text(po_path)) - assert not matches, ( - f"{po_path.name}: found '@...;' entity typo(s): {', '.join(sorted(set(matches)))}" - ) diff --git a/sunnypilot/common/version.h b/sunnypilot/common/version.h deleted file mode 100644 index fccd807e64..0000000000 --- a/sunnypilot/common/version.h +++ /dev/null @@ -1 +0,0 @@ -#define SUNNYPILOT_VERSION "2026.002.000" diff --git a/sunnypilot/models/tests/model_hash b/sunnypilot/models/tests/model_hash deleted file mode 100644 index f363f8309a..0000000000 --- a/sunnypilot/models/tests/model_hash +++ /dev/null @@ -1 +0,0 @@ -32f57bdc91f910df1f48ddae7c59aaf6e751f9df6756da481a210577dbce8bcf \ No newline at end of file diff --git a/sunnypilot/selfdrive/car/car_list.json b/sunnypilot/selfdrive/car/car_list.json deleted file mode 120000 index a4ae7b75f9..0000000000 --- a/sunnypilot/selfdrive/car/car_list.json +++ /dev/null @@ -1 +0,0 @@ -../../../opendbc_repo/opendbc/sunnypilot/car/car_list.json \ No newline at end of file diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json deleted file mode 100644 index 764f37ffde..0000000000 --- a/sunnypilot/sunnylink/params_metadata.json +++ /dev/null @@ -1,1411 +0,0 @@ -{ - "AccessToken": { - "title": "AccessTokenIsNice", - "description": "" - }, - "AdbEnabled": { - "title": "Enable ADB", - "description": "" - }, - "AlphaLongitudinalEnabled": { - "title": "Alpha Longitudinal", - "description": "" - }, - "AlwaysOnDM": { - "title": "Always-on Driver Monitor", - "description": "" - }, - "ApiCache_Device": { - "title": "Api Cache Device", - "description": "" - }, - "ApiCache_DriveStats": { - "title": "Api Cache Drive Stats", - "description": "" - }, - "ApiCache_FirehoseStats": { - "title": "Firehose Mode Stats", - "description": "" - }, - "AssistNowToken": { - "title": "Assist Now Token", - "description": "" - }, - "AthenadPid": { - "title": "Athenad Pid", - "description": "" - }, - "AthenadRecentlyViewedRoutes": { - "title": "Athenad Recently Viewed Routes", - "description": "" - }, - "AthenadUploadQueue": { - "title": "Athenad Upload Queue", - "description": "" - }, - "AutoLaneChangeBsmDelay": { - "title": "Auto Lane Change BSM Delay", - "description": "" - }, - "AutoLaneChangeTimer": { - "title": "Auto Lane Change Timer", - "description": "", - "options": [ - { - "value": -1, - "label": "Off" - }, - { - "value": 0, - "label": "Nudge" - }, - { - "value": 1, - "label": "Nudgeless" - }, - { - "value": 2, - "label": "0.5s" - }, - { - "value": 3, - "label": "1s" - }, - { - "value": 4, - "label": "2s" - }, - { - "value": 5, - "label": "3s" - } - ] - }, - "BackupManager_CreateBackup": { - "title": "Create Backup", - "description": "" - }, - "BackupManager_RestoreVersion": { - "title": "Restore Version", - "description": "" - }, - "BlindSpot": { - "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", - "description": "" - }, - "BlinkerPauseLateralControl": { - "title": "Blinker Pause Lateral Control", - "description": "" - }, - "BootCount": { - "title": "Boot Count", - "description": "" - }, - "Brightness": { - "title": "Screen Brightness", - "description": "" - }, - "CalibrationParams": { - "title": "Calibration Params", - "description": "" - }, - "CameraDebugExpGain": { - "title": "Camera Debug Exp Gain", - "description": "" - }, - "CameraDebugExpTime": { - "title": "Camera Debug Exp Time", - "description": "" - }, - "CameraOffset": { - "title": "Adjust Camera Offset", - "description": "Virtually shift camera's perspective to move model's center to Left(+ values) or Right (- values)", - "min": -0.35, - "max": 0.35, - "step": 0.01, - "unit": "meters" - }, - "CarBatteryCapacity": { - "title": "Car Battery Capacity", - "description": "Battery Size", - "unit": "kWh" - }, - "CarList": { - "title": "Supported Car List", - "description": "All supported platform in sunnypilot" - }, - "CarParams": { - "title": "Car Params", - "description": "" - }, - "CarParamsCache": { - "title": "Car Params Cache", - "description": "" - }, - "CarParamsPersistent": { - "title": "Car Params Persistent", - "description": "" - }, - "CarParamsPrevRoute": { - "title": "Car Params Prev Route", - "description": "" - }, - "CarParamsSP": { - "title": "Car Params Sp", - "description": "" - }, - "CarParamsSPCache": { - "title": "Car Params Sp Cache", - "description": "" - }, - "CarParamsSPPersistent": { - "title": "Car Params Sp Persistent", - "description": "" - }, - "CarPlatformBundle": { - "title": "Car Platform Bundle", - "description": "" - }, - "ChevronInfo": { - "title": "Chevron Info", - "description": "" - }, - "CompletedSunnylinkConsentVersion": { - "title": "Completed sunnylink Consent Version", - "description": "" - }, - "CompletedTrainingVersion": { - "title": "Completed Training Version", - "description": "" - }, - "ControlsReady": { - "title": "Controls Ready", - "description": "" - }, - "CurrentBootlog": { - "title": "Current Bootlog", - "description": "" - }, - "CurrentRoute": { - "title": "Current Route", - "description": "" - }, - "CustomAccIncrementsEnabled": { - "title": "Custom ACC Increments", - "description": "" - }, - "CustomAccLongPressIncrement": { - "title": "Custom ACC Long Press Increment", - "description": "", - "min": 1, - "max": 10, - "step": 1 - }, - "CustomAccShortPressIncrement": { - "title": "Custom ACC Short Press Increment", - "description": "", - "min": 1, - "max": 10, - "step": 1 - }, - "CustomTorqueParams": { - "title": "Enable Custom Torque Tuning", - "description": "Enables custom tuning for Torque lateral control" - }, - "DevUIInfo": { - "title": "Developer UI Info", - "description": "" - }, - "DeviceBootMode": { - "title": "Device Boot Mode", - "description": "", - "options": [ - { - "value": 0, - "label": "Standard" - }, - { - "value": 1, - "label": "Always Offroad" - } - ] - }, - "DisableLogging": { - "title": "Disable Logging", - "description": "" - }, - "DisablePowerDown": { - "title": "Disable Power Down", - "description": "" - }, - "DisableUpdates": { - "title": "Disable Updates", - "description": "" - }, - "DisengageOnAccelerator": { - "title": "Disengage On Accelerator", - "description": "" - }, - "DoReboot": { - "title": "Reboot", - "description": "" - }, - "DoShutdown": { - "title": "Power Off", - "description": "" - }, - "DoUninstall": { - "title": "Uninstall sunnypilot", - "description": "" - }, - "DongleId": { - "title": "Device ID", - "description": "" - }, - "DriverTooDistracted": { - "title": "Driver Too Distracted", - "description": "" - }, - "DynamicExperimentalControl": { - "title": "Dynamic Experimental Control", - "description": "" - }, - "EnableCopyparty": { - "title": "copyparty Service", - "description": "" - }, - "EnableGithubRunner": { - "title": "GitHub Runner Service", - "description": "" - }, - "EnableSunnylinkUploader": { - "title": "Enable sunnylink Uploader", - "description": "" - }, - "EnforceTorqueControl": { - "title": "Enforce Torque Control", - "description": "Enable this to enforce sunnypilot to steer with Torque lateral control." - }, - "ExperimentalMode": { - "title": "Experimental Mode", - "description": "" - }, - "ExperimentalModeConfirmed": { - "title": "Experimental Mode Confirmed", - "description": "" - }, - "FirmwareQueryDone": { - "title": "Firmware Query Done", - "description": "" - }, - "ForcePowerDown": { - "title": "Force Power Down", - "description": "" - }, - "GitBranch": { - "title": "Git Branch", - "description": "" - }, - "GitCommit": { - "title": "Git Commit", - "description": "" - }, - "GitCommitDate": { - "title": "Git Commit Date", - "description": "" - }, - "GitDiff": { - "title": "Git Diff", - "description": "" - }, - "GitRemote": { - "title": "Git Remote", - "description": "" - }, - "GithubRunnerSufficientVoltage": { - "title": "Github Runner Sufficient Voltage", - "description": "" - }, - "GithubSshKeys": { - "title": "Github Ssh Keys", - "description": "" - }, - "GithubUsername": { - "title": "GitHub Username", - "description": "" - }, - "GreenLightAlert": { - "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", - "description": "" - }, - "GsmMetered": { - "title": "Gsm Metered", - "description": "" - }, - "GsmRoaming": { - "title": "GSM Roaming", - "description": "" - }, - "HardwareSerial": { - "title": "Serial Number", - "description": "" - }, - "HasAcceptedTerms": { - "title": "Has Accepted Terms", - "description": "" - }, - "HasAcceptedTermsSP": { - "title": "Has Accepted sunnypilot Terms", - "description": "" - }, - "HideVEgoUI": { - "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", - "description": "", - "options": [ - { - "value": 0, - "label": "Off" - }, - { - "value": 1, - "label": "Dynamic" - }, - { - "value": 2, - "label": "Predictive" - } - ] - }, - "InstallDate": { - "title": "Install Date", - "description": "" - }, - "IntelligentCruiseButtonManagement": { - "title": "Intelligent Cruise Button Management", - "description": "" - }, - "InteractivityTimeout": { - "title": "Interactivity Timeout", - "description": "Apply a custom timeout for settings UI. This is the time after which settings UI closes automatically if user is not interacting with the screen.", - "options": [ - { - "value": 0, - "label": "Default" - }, - { - "value": 10, - "label": "10 s" - }, - { - "value": 20, - "label": "20 s" - }, - { - "value": 30, - "label": "30 s" - }, - { - "value": 40, - "label": "40 s" - }, - { - "value": 50, - "label": "50 s" - }, - { - "value": 60, - "label": "1 m" - }, - { - "value": 70, - "label": "1 m" - }, - { - "value": 80, - "label": "1 m" - }, - { - "value": 90, - "label": "1 m" - }, - { - "value": 100, - "label": "1 m" - }, - { - "value": 110, - "label": "1 m" - }, - { - "value": 120, - "label": "2 m" - } - ] - }, - "IsDevelopmentBranch": { - "title": "Is Development Branch", - "description": "" - }, - "IsDriverViewEnabled": { - "title": "Is Driver View Enabled", - "description": "" - }, - "IsEngaged": { - "title": "Is Engaged", - "description": "" - }, - "IsLdwEnabled": { - "title": "Lane Departure Warnings", - "description": "" - }, - "IsMetric": { - "title": "Use Metric Units", - "description": "" - }, - "IsOffroad": { - "title": "Is Offroad", - "description": "" - }, - "IsOnroad": { - "title": "Is Onroad", - "description": "" - }, - "IsReleaseBranch": { - "title": "Is Release Branch", - "description": "" - }, - "IsReleaseSpBranch": { - "title": "Is Release Sp Branch", - "description": "" - }, - "IsRhdDetected": { - "title": "Is Rhd Detected", - "description": "" - }, - "IsTakingSnapshot": { - "title": "Is Taking Snapshot", - "description": "" - }, - "IsTestedBranch": { - "title": "Is Tested Branch", - "description": "" - }, - "JoystickDebugMode": { - "title": "Joystick Debug Mode", - "description": "" - }, - "LagdToggle": { - "title": "Live Learning Steer Delay", - "description": "Allow device to learn and adapt car's steering response time" - }, - "LagdToggleDelay": { - "title": "Manual Software Delay", - "description": "Software delay to use when Live Learning Steer Delay is toggled off", - "min": 0.05, - "max": 0.5, - "step": 0.01 - }, - "LagdValueCache": { - "title": "LaGD Value Cache", - "description": "" - }, - "LaneTurnDesire": { - "title": "Lane Turn Desire", - "description": "Force model to plan an intent to turn based on blinker" - }, - "LaneTurnValue": { - "title": "Lane Turn Speed", - "description": "Maximum speed for lane turn desire", - "min": 0, - "max": 20, - "step": 1 - }, - "LanguageSetting": { - "title": "Language", - "description": "" - }, - "LastAgnosPowerMonitorShutdown": { - "title": "Last AGNOS Power Monitor Shutdown", - "description": "" - }, - "LastAthenaPingTime": { - "title": "Last Athena Ping Time", - "description": "" - }, - "LastGPSPosition": { - "title": "Last Gps Position", - "description": "" - }, - "LastGPSPositionLLK": { - "title": "Last GPS Position LLK", - "description": "" - }, - "LastManagerExitReason": { - "title": "Last Manager Exit Reason", - "description": "" - }, - "LastOffroadStatusPacket": { - "title": "Last Offroad Status Packet", - "description": "" - }, - "LastPowerDropDetected": { - "title": "Last Power Drop Detected", - "description": "" - }, - "LastSunnylinkPingTime": { - "title": "Last sunnylink Ping Time", - "description": "" - }, - "LastUpdateException": { - "title": "Last Update Exception", - "description": "" - }, - "LastUpdateRouteCount": { - "title": "Last Update Route Count", - "description": "" - }, - "LastUpdateTime": { - "title": "Last Update Time", - "description": "" - }, - "LastUpdateUptimeOnroad": { - "title": "Last Update Uptime Onroad", - "description": "" - }, - "LateralManeuverMode": { - "title": "Lateral Maneuver Mode", - "description": "" - }, - "LeadDepartAlert": { - "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", - "description": "" - }, - "LiveParameters": { - "title": "Live Parameters", - "description": "" - }, - "LiveParametersV2": { - "title": "Live Parameters V2", - "description": "" - }, - "LiveTorqueParameters": { - "title": "Live Torque Parameters", - "description": "" - }, - "LiveTorqueParamsRelaxedToggle": { - "title": "Less Restrict Settings for Self-Tune (Beta)", - "description": "Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values." - }, - "LiveTorqueParamsToggle": { - "title": "Self-Tune", - "description": "Enables self-tune for Torque lateral control" - }, - "LivestreamEncoderBitrate": { - "title": "Livestream Encoder Bitrate", - "description": "" - }, - "LocationFilterInitialState": { - "title": "Location Filter Initial State", - "description": "" - }, - "LongitudinalManeuverMode": { - "title": "Longitudinal Maneuver Mode", - "description": "" - }, - "LongitudinalPersonality": { - "title": "Driving Personality", - "description": "", - "options": [ - { - "value": 0, - "label": "Aggressive" - }, - { - "value": 1, - "label": "Standard" - }, - { - "value": 2, - "label": "Relaxed" - } - ] - }, - "Mads": { - "title": "MADS Enabled", - "description": "" - }, - "MadsMainCruiseAllowed": { - "title": "MADS Main Cruise Allowed", - "description": "" - }, - "MadsSteeringMode": { - "title": "MADS Steering Mode", - "description": "", - "options": [ - { - "value": 0, - "label": "Remain Active" - }, - { - "value": 1, - "label": "Pause" - }, - { - "value": 2, - "label": "Disengage" - } - ] - }, - "MadsUnifiedEngagementMode": { - "title": "MADS Unified Engagement Mode", - "description": "" - }, - "MapAdvisorySpeedLimit": { - "title": "Map Advisory Speed Limit", - "description": "" - }, - "MapSpeedLimit": { - "title": "Map Speed Limit", - "description": "" - }, - "MapTargetVelocities": { - "title": "Map Target Velocities", - "description": "" - }, - "MapdVersion": { - "title": "Mapd Version", - "description": "" - }, - "MaxTimeOffroad": { - "title": "Max Time Offroad", - "description": "", - "unit": "minutes" - }, - "ModelManager_ActiveBundle": { - "title": "Model Manager Active Bundle", - "description": "" - }, - "ModelManager_ClearCache": { - "title": "Model Manager Clear Cache", - "description": "" - }, - "ModelManager_DownloadIndex": { - "title": "Model Manager Download Index", - "description": "" - }, - "ModelManager_Favs": { - "title": "Model Manager Favorites", - "description": "" - }, - "ModelManager_LastSyncTime": { - "title": "Model Manager Last Sync Time", - "description": "" - }, - "ModelManager_ModelsCache": { - "title": "Model Manager Models Cache", - "description": "" - }, - "ModelRunnerTypeCache": { - "title": "Model Runner Type Cache", - "description": "" - }, - "NetworkMetered": { - "title": "Network Usage", - "description": "", - "options": [ - { - "value": 0, - "label": "Default" - }, - { - "value": 1, - "label": "Metered" - }, - { - "value": 2, - "label": "Unmetered" - } - ] - }, - "NeuralNetworkLateralControl": { - "title": "Neural Network Lateral Control", - "description": "" - }, - "NextMapSpeedLimit": { - "title": "Next Map Speed Limit", - "description": "" - }, - "OSMDownloadBounds": { - "title": "OSM Download Bounds", - "description": "" - }, - "OSMDownloadLocations": { - "title": "OSM Download Locations", - "description": "" - }, - "OSMDownloadProgress": { - "title": "OSM Download Progress", - "description": "" - }, - "ObdMultiplexingChanged": { - "title": "Obd Multiplexing Changed", - "description": "" - }, - "ObdMultiplexingEnabled": { - "title": "Obd Multiplexing Enabled", - "description": "" - }, - "OffroadMode": { - "title": "Force Offroad Mode", - "description": "" - }, - "Offroad_CarUnrecognized": { - "title": "Offroad Car Unrecognized", - "description": "" - }, - "Offroad_ConnectivityNeeded": { - "title": "Offroad Connectivity Needed", - "description": "" - }, - "Offroad_ConnectivityNeededPrompt": { - "title": "Offroad Connectivity Needed Prompt", - "description": "" - }, - "Offroad_DriverMonitoringUncertain": { - "title": "Offroad Driver Monitoring Uncertain", - "description": "" - }, - "Offroad_ExcessiveActuation": { - "title": "Offroad Excessive Actuation", - "description": "" - }, - "Offroad_IsTakingSnapshot": { - "title": "Offroad Is Taking Snapshot", - "description": "" - }, - "Offroad_NeosUpdate": { - "title": "Offroad Neos Update", - "description": "" - }, - "Offroad_NoFirmware": { - "title": "Offroad No Firmware", - "description": "" - }, - "Offroad_OSMUpdateRequired": { - "title": "Offroad OSM Update Required", - "description": "" - }, - "Offroad_Recalibration": { - "title": "Offroad Recalibration", - "description": "" - }, - "Offroad_TemperatureTooHigh": { - "title": "Offroad Temperature Too High", - "description": "" - }, - "Offroad_TiciSupport": { - "title": "Offroad Tici Support", - "description": "" - }, - "Offroad_UnregisteredHardware": { - "title": "Offroad Unregistered Hardware", - "description": "" - }, - "Offroad_UpdateFailed": { - "title": "Offroad Update Failed", - "description": "" - }, - "OnroadCycleRequested": { - "title": "Onroad Cycle Requested", - "description": "" - }, - "OnroadScreenOffBrightness": { - "title": "Onroad Brightness", - "description": "", - "options": [ - { - "value": 0, - "label": "Auto (Default)" - }, - { - "value": 1, - "label": "Auto (Dark)" - }, - { - "value": 2, - "label": "Screen Off" - }, - { - "value": 3, - "label": "5 %" - }, - { - "value": 4, - "label": "10 %" - }, - { - "value": 5, - "label": "15 %" - }, - { - "value": 6, - "label": "20 %" - }, - { - "value": 7, - "label": "25 %" - }, - { - "value": 8, - "label": "30 %" - }, - { - "value": 9, - "label": "35 %" - }, - { - "value": 10, - "label": "40 %" - }, - { - "value": 11, - "label": "45 %" - }, - { - "value": 12, - "label": "50 %" - }, - { - "value": 13, - "label": "55 %" - }, - { - "value": 14, - "label": "60 %" - }, - { - "value": 15, - "label": "65 %" - }, - { - "value": 16, - "label": "70 %" - }, - { - "value": 17, - "label": "75 %" - }, - { - "value": 18, - "label": "80 %" - }, - { - "value": 19, - "label": "85 %" - }, - { - "value": 20, - "label": "90 %" - }, - { - "value": 21, - "label": "95 %" - }, - { - "value": 22, - "label": "100 %" - } - ] - }, - "OnroadScreenOffBrightnessMigrated": { - "title": "Onroad Brightness Migration Version", - "description": "This param is to track whether OnroadScreenOffBrightness needs to be migrated." - }, - "OnroadScreenOffControl": { - "title": "Onroad Brightness", - "description": "Adjusts the screen brightness while it's in onroad state." - }, - "OnroadScreenOffTimer": { - "title": "Onroad Brightness Delay", - "description": "", - "options": [ - { - "value": 3, - "label": "3s" - }, - { - "value": 5, - "label": "5s" - }, - { - "value": 7, - "label": "7s" - }, - { - "value": 10, - "label": "10s" - }, - { - "value": 15, - "label": "15s" - }, - { - "value": 30, - "label": "30s" - }, - { - "value": 60, - "label": "1m" - }, - { - "value": 120, - "label": "2m" - }, - { - "value": 180, - "label": "3m" - }, - { - "value": 240, - "label": "4m" - }, - { - "value": 300, - "label": "5m" - }, - { - "value": 360, - "label": "6m" - }, - { - "value": 420, - "label": "7m" - }, - { - "value": 480, - "label": "8m" - }, - { - "value": 540, - "label": "9m" - }, - { - "value": 600, - "label": "10m" - } - ] - }, - "OnroadScreenOffTimerMigrated": { - "title": "Onroad Brightness Delay Migration Version", - "description": "This param is to track whether OnroadScreenOffTimer needs to be migrated." - }, - "OnroadUploads": { - "title": "Onroad Uploads", - "description": "" - }, - "OpenpilotEnabledToggle": { - "title": "Enable sunnypilot", - "description": "" - }, - "OsmDbUpdatesCheck": { - "title": "OSM DB Updates Check", - "description": "" - }, - "OsmDownloadedDate": { - "title": "OSM Downloaded Date", - "description": "" - }, - "OsmLocal": { - "title": "OSM Local", - "description": "" - }, - "OsmLocationName": { - "title": "OSM Location Name", - "description": "" - }, - "OsmLocationTitle": { - "title": "OSM Location Title", - "description": "" - }, - "OsmLocationUrl": { - "title": "OSM Location URL", - "description": "" - }, - "OsmStateName": { - "title": "OSM State Name", - "description": "" - }, - "OsmStateTitle": { - "title": "OSM State Title", - "description": "" - }, - "OsmWayTest": { - "title": "OSM Way Test", - "description": "" - }, - "PandaHeartbeatLost": { - "title": "Panda Heartbeat Lost", - "description": "" - }, - "PandaSignatures": { - "title": "Panda Signatures", - "description": "" - }, - "PandaSomResetTriggered": { - "title": "Panda Som Reset Triggered", - "description": "" - }, - "ParamsVersion": { - "title": "Params Version", - "description": "" - }, - "PlanplusControl": { - "title": "Plan Plus Controls", - "description": "Adjust planplus model recentering strength. The higher this number the more aggressively the model will recover to lanecenter, too high and it will ping-pong", - "min": 0.0, - "max": 2.0, - "step": 0.1 - }, - "PrimeType": { - "title": "Prime Type", - "description": "" - }, - "QuickBootToggle": { - "title": "Quick Boot", - "description": "" - }, - "QuietMode": { - "title": "Quiet Mode", - "description": "" - }, - "RainbowMode": { - "title": "Rainbow Mode", - "description": "" - }, - "RecordAudio": { - "title": "Record & Upload Mic Audio", - "description": "" - }, - "RecordAudioFeedback": { - "title": "Record Audio Feedback", - "description": "" - }, - "RecordFront": { - "title": "Record & Upload Driver Camera", - "description": "" - }, - "RecordFrontLock": { - "title": "Record Front Lock", - "description": "" - }, - "RoadName": { - "title": "Road Name", - "description": "" - }, - "RoadNameToggle": { - "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": "[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", - "description": "" - }, - "SecOCKey": { - "title": "Sec Oc Key", - "description": "" - }, - "ShowAdvancedControls": { - "title": "Show Advanced Controls", - "description": "Enable to show advanced controls on device" - }, - "ShowDebugInfo": { - "title": "UI Debug Mode", - "description": "" - }, - "ShowTurnSignals": { - "title": "[TIZI/TICI only] Display Turn Signals", - "description": "When enabled, visual turn indicators are drawn on the HUD." - }, - "SmartCruiseControlMap": { - "title": "Smart Cruise Control - Map", - "description": "" - }, - "SmartCruiseControlVision": { - "title": "Smart Cruise Control - Vision", - "description": "" - }, - "SnoozeUpdate": { - "title": "Snooze Update", - "description": "" - }, - "SpeedLimitMode": { - "title": "Speed Limit Assist Mode", - "description": "", - "options": [ - { - "value": 0, - "label": "Off" - }, - { - "value": 1, - "label": "Information" - }, - { - "value": 2, - "label": "Warning" - }, - { - "value": 3, - "label": "Assist" - } - ] - }, - "SpeedLimitOffsetType": { - "title": "Speed Limit Offset Type", - "description": "", - "options": [ - { - "value": 0, - "label": "Off" - }, - { - "value": 1, - "label": "Fixed" - }, - { - "value": 2, - "label": "Percentage" - } - ] - }, - "SpeedLimitPolicy": { - "title": "Speed Limit Source", - "description": "", - "options": [ - { - "value": 0, - "label": "Car State Only" - }, - { - "value": 1, - "label": "Map Data Only" - }, - { - "value": 2, - "label": "Car State Priority" - }, - { - "value": 3, - "label": "Map Data Priority" - }, - { - "value": 4, - "label": "Combined" - } - ] - }, - "SpeedLimitValueOffset": { - "title": "Speed Limit Offset Value", - "description": "", - "min": -30, - "max": 30, - "step": 1 - }, - "SshEnabled": { - "title": "Enable SSH", - "description": "" - }, - "StandstillTimer": { - "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", - "description": "" - }, - "SubaruStopAndGoManualParkingBrake": { - "title": "Subaru Stop and Go Manual Parking Brake", - "description": "" - }, - "SunnylinkCache_Roles": { - "title": "sunnylink Cache Roles", - "description": "" - }, - "SunnylinkCache_Users": { - "title": "sunnylink Cache Users", - "description": "" - }, - "SunnylinkDongleId": { - "title": "sunnylink Dongle ID", - "description": "" - }, - "SunnylinkEnabled": { - "title": "sunnylink Enabled", - "description": "" - }, - "SunnylinkTempFault": { - "title": "sunnylink Temp Fault", - "description": "" - }, - "SunnylinkdPid": { - "title": "Sunnylinkd Pid", - "description": "" - }, - "TermsVersion": { - "title": "Terms Version", - "description": "" - }, - "TeslaCoopSteering": { - "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." - }, - "TorqueControlTune": { - "title": "Torque Control Tune Version", - "description": "Select the version of Torque Control Tune to use.", - "options": [ - { - "value": "", - "label": "Default" - }, - { - "value": 1.0, - "label": "v1.0" - }, - { - "value": 0.0, - "label": "v0.0" - } - ] - }, - "TorqueParamsOverrideEnabled": { - "title": "Manual Real-Time Tuning", - "description": "" - }, - "TorqueParamsOverrideFriction": { - "title": "Manual Tune - Friction", - "description": "", - "min": 0.0, - "max": 1.0, - "step": 0.01 - }, - "TorqueParamsOverrideLatAccelFactor": { - "title": "Manual Tune - Lateral Acceleration Factor", - "description": "", - "min": 0.1, - "max": 5.0, - "step": 0.1, - "unit": "m/s\u00b2" - }, - "ToyotaEnforceStockLongitudinal": { - "title": "Toyota: Enforce Factory Longitudinal Control", - "description": "When enabled, sunnypilot will not take over control of gas and brakes. Factory Toyota longitudinal control will be used." - }, - "ToyotaStopAndGoHack": { - "title": "Toyota: Stop and Go Hack (Alpha)", - "description": "sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. This feature is only applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk." - }, - "TrainingVersion": { - "title": "Training Version", - "description": "" - }, - "TrueVEgoUI": { - "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", - "description": "" - }, - "UpdateAvailable": { - "title": "Update Available", - "description": "" - }, - "UpdateFailedCount": { - "title": "Update Failed Count", - "description": "" - }, - "UpdaterAvailableBranches": { - "title": "Updater Available Branches", - "description": "" - }, - "UpdaterCurrentDescription": { - "title": "Updater Current Description", - "description": "" - }, - "UpdaterCurrentReleaseNotes": { - "title": "Updater Current Release Notes", - "description": "" - }, - "UpdaterFetchAvailable": { - "title": "Updater Fetch Available", - "description": "" - }, - "UpdaterLastFetchTime": { - "title": "Updater Last Fetch Time", - "description": "" - }, - "UpdaterNewDescription": { - "title": "Updater New Description", - "description": "" - }, - "UpdaterNewReleaseNotes": { - "title": "Updater New Release Notes", - "description": "" - }, - "UpdaterState": { - "title": "Updater State", - "description": "" - }, - "UpdaterTargetBranch": { - "title": "Updater Target Branch", - "description": "" - }, - "UptimeOffroad": { - "title": "Uptime Offroad", - "description": "" - }, - "UptimeOnroad": { - "title": "Uptime Onroad", - "description": "" - }, - "UsbGpuCompiled": { - "title": "Usb Gpu Compiled", - "description": "" - }, - "UsbGpuPresent": { - "title": "Usb Gpu Present", - "description": "" - }, - "Version": { - "title": "openpilot Version", - "description": "" - } -} diff --git a/sunnypilot/sunnylink/tests/test_params_metadata.py b/sunnypilot/sunnylink/tests/test_params_metadata.py deleted file mode 100644 index f4f1fbc4b1..0000000000 --- a/sunnypilot/sunnylink/tests/test_params_metadata.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" -import json - -from openpilot.sunnypilot.sunnylink.athena.sunnylinkd import getParamsAllKeysV1, METADATA_PATH - - -def test_get_params_all_keys_v1(): - """ - Test the getParamsAllKeysV1 API endpoint. - - Why: - This endpoint is used by the UI (and potentially external tools) to fetch the list of - available parameters along with their metadata (titles, descriptions, options, constraints). - We need to ensure it returns the correct structure and that the metadata from - params_metadata.json is correctly merged into the response. - - Expected: - - The response should contain a "keys" field which is a JSON string of a list of parameters. - - Each parameter object should have "key", "type", "default_value", and optionally "_extra". - - The "_extra" field should contain the rich metadata (title, options, min/max, etc.) matching - the source of truth (params_metadata.json). - """ - response = getParamsAllKeysV1() - assert "keys" in response - - keys_json = response["keys"] - params_list = json.loads(keys_json) - - assert isinstance(params_list, list) - assert len(params_list) > 0 - - # Check structure of first item - first_param = params_list[0] - assert "key" in first_param - assert "type" in first_param - assert "default_value" in first_param - - if "_extra" in first_param: - assert isinstance(first_param["_extra"], dict) - assert "default" not in first_param["_extra"] - assert "type" not in first_param["_extra"] - - # Load the source of truth - with open(METADATA_PATH) as f: - metadata = json.load(f) - - # Verify that the API response matches the metadata file for a few sample keys - # This ensures the plumbing is working without being brittle to content changes - - # 1. Check a key that should have metadata - keys_with_metadata = [k for k in params_list if k["key"] in metadata] - assert len(keys_with_metadata) > 0, "No parameters found that match metadata keys" - - for param in keys_with_metadata[:5]: # Check first 5 matches - key = param["key"] - expected_meta = metadata[key] - - assert "_extra" in param, f"Parameter {key} should have _extra field" - actual_meta = param["_extra"] - - # Verify all fields in JSON are present in the API response - for meta_key, meta_val in expected_meta.items(): - assert meta_key in actual_meta, f"Missing {meta_key} in API response for {key}" - assert actual_meta[meta_key] == meta_val, f"Mismatch for {key}.{meta_key}: expected {meta_val}, got {actual_meta[meta_key]}" - - # 2. Check that we are correctly serving options if they exist - params_with_options = [k for k in keys_with_metadata if "options" in k.get("_extra", {})] - if params_with_options: - param = params_with_options[0] - key = param["key"] - assert isinstance(param["_extra"]["options"], list), f"Options for {key} should be a list" - assert param["_extra"]["options"] == metadata[key]["options"] - - # 3. Check that we are correctly serving numeric constraints if they exist - params_with_constraints = [k for k in keys_with_metadata if "min" in k.get("_extra", {})] - if params_with_constraints: - param = params_with_constraints[0] - key = param["key"] - assert param["_extra"]["min"] == metadata[key]["min"] - assert param["_extra"]["max"] == metadata[key]["max"] - assert param["_extra"]["step"] == metadata[key]["step"] diff --git a/sunnypilot/sunnylink/tests/test_params_sync.py b/sunnypilot/sunnylink/tests/test_params_sync.py deleted file mode 100644 index 05114de49a..0000000000 --- a/sunnypilot/sunnylink/tests/test_params_sync.py +++ /dev/null @@ -1,284 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" -import json -import os -import pytest - -from openpilot.common.params import Params -from openpilot.sunnypilot.sunnylink.athena.sunnylinkd import METADATA_PATH - - -def test_metadata_json_exists(): - """ - Test that the params_metadata.json file exists at the expected path. - - Why: - The metadata file is the source of truth for parameter descriptions, options, and constraints. - If it's missing, the UI will not be able to display rich information for parameters. - - Expected: - The file should exist at sunnypilot/sunnylink/params_metadata.json. - """ - assert os.path.exists(METADATA_PATH), f"Metadata file not found at {METADATA_PATH}" - - -def test_metadata_json_valid(): - """ - Test that the params_metadata.json file contains valid JSON. - - Why: - Invalid JSON will cause the metadata loading to fail, potentially crashing the UI or - resulting in missing metadata. - - Expected: - The file content should be parseable as a JSON object (dictionary). - """ - with open(METADATA_PATH) as f: - try: - data = json.load(f) - except json.JSONDecodeError: - pytest.fail("Metadata file is not valid JSON") - - assert isinstance(data, dict), "Metadata root must be a dictionary" - - -def test_all_params_have_metadata(): - """ - Test that every parameter in the codebase has a corresponding entry in params_metadata.json. - - Why: - We want to ensure 100% coverage of parameter metadata. Any parameter added to the codebase - should also be documented in the metadata file. - - Expected: - There should be no parameters in Params() that are missing from the metadata file. - If this fails, run 'python3 sunnypilot/sunnylink/tools/update_params_metadata.py'. - """ - params = Params() - all_keys = [k.decode('utf-8') for k in params.all_keys()] - - with open(METADATA_PATH) as f: - metadata = json.load(f) - - missing_keys = [key for key in all_keys if key not in metadata] - - if missing_keys: - pytest.fail( - f"The following parameters are missing from metadata: {missing_keys}. " - + "Please run 'python3 sunnypilot/sunnylink/tools/update_params_metadata.py' to update." - ) - - -def test_metadata_keys_exist_in_params(): - """ - Test that all keys in params_metadata.json actually exist in the codebase. - - Why: - We want to avoid stale metadata for parameters that have been removed or renamed. - This keeps the metadata file clean and relevant. - - Expected: - There should be no keys in the metadata file that are not present in Params(). - This prints a warning rather than failing, as it's less critical than missing metadata. - """ - params = Params() - all_keys = {k.decode('utf-8') for k in params.all_keys()} - - with open(METADATA_PATH) as f: - metadata = json.load(f) - - extra_keys = [key for key in metadata.keys() if key not in all_keys] - - if extra_keys: - print(f"Warning: The following keys in metadata do not exist in Params: {extra_keys}") - - -def test_no_default_titles(): - """ - Test that no parameter has a title that is identical to its key. - - Why: - The default behavior of the update script is to set the title equal to the key. - We want to force developers to provide human-readable, descriptive titles for all parameters. - - Expected: - No parameter metadata should have 'title' == 'key'. - """ - with open(METADATA_PATH) as f: - metadata = json.load(f) - - default_title_keys = [key for key, meta in metadata.items() if meta.get("title") == key] - - if default_title_keys: - pytest.fail( - f"The following parameters have default titles (title == key): {default_title_keys}. " - + "Please update 'params_metadata.json' with descriptive titles." - ) - - -def test_options_structure(): - """ - Test that the 'options' field in metadata follows the correct structure. - - Why: - The UI expects 'options' to be a list of objects with 'value' and 'label' keys. - Incorrect structure will break the UI rendering for dropdowns/toggles. - - Expected: - If 'options' is present, it must be a list of dicts, and each dict must have 'value' and 'label'. - """ - with open(METADATA_PATH) as f: - metadata = json.load(f) - - for key, meta in metadata.items(): - if "options" in meta: - options = meta["options"] - assert isinstance(options, list), f"Options for {key} must be a list" - for option in options: - assert isinstance(option, dict), f"Option in {key} must be a dictionary" - assert "value" in option, f"Option in {key} must have a 'value' key" - assert "label" in option, f"Option in {key} must have a 'label' key" - - -def test_numeric_constraints(): - """ - Test that numeric parameters have valid 'min', 'max', and 'step' constraints. - - Why: - The UI uses these constraints to validate user input and render sliders/steppers. - Missing or invalid constraints can lead to UI bugs or invalid parameter values. - - Expected: - If any of min/max/step is present, ALL of them must be present. - They must be numbers (int/float), and min must be less than max. - """ - with open(METADATA_PATH) as f: - metadata = json.load(f) - - for key, meta in metadata.items(): - if "min" in meta or "max" in meta or "step" in meta: - assert "min" in meta, f"Numeric param {key} must have 'min'" - assert "max" in meta, f"Numeric param {key} must have 'max'" - assert "step" in meta, f"Numeric param {key} must have 'step'" - - assert isinstance(meta["min"], (int, float)), f"Min for {key} must be number" - assert isinstance(meta["max"], (int, float)), f"Max for {key} must be number" - assert isinstance(meta["step"], (int, float)), f"Step for {key} must be number" - assert meta["min"] < meta["max"], f"Min must be less than max for {key}" - - -def test_known_params_metadata(): - """ - Test specific known parameters to ensure they have the expected rich metadata. - - Why: - This acts as a spot check to ensure that our rich metadata population logic is working correctly - and that critical parameters (like LongitudinalPersonality) have their options and constraints preserved. - - Expected: - 'LongitudinalPersonality' should have 3 options (Aggressive, Standard, Relaxed). - 'CustomAccLongPressIncrement' should have min=1, max=10, step=1. - """ - with open(METADATA_PATH) as f: - metadata = json.load(f) - - # Check an enum-like param - lp = metadata.get("LongitudinalPersonality") - assert lp is not None - assert "options" in lp - assert len(lp["options"]) == 3 - assert lp["options"][0]["label"] == "Aggressive" - assert lp["options"][0]["value"] == 0 - - # Check a numeric param - acc_long = metadata.get("CustomAccLongPressIncrement") - assert acc_long is not None - assert acc_long["min"] == 1 - assert acc_long["max"] == 10 - assert acc_long["step"] == 1 - - -def test_torque_control_tune_versions_in_sync(): - """ - Test that TorqueControlTune options in params_metadata.json match versions in latcontrol_torque_versions.json. - - Why: - The TorqueControlTune dropdown in the UI should always reflect the available torque tune versions. - If versions are added/removed from latcontrol_torque_versions.json, the metadata must be updated accordingly. - - Expected: - - TorqueControlTune should have a 'Default' option with empty string value - - All versions from latcontrol_torque_versions.json should be present in the options - - The version values and labels should match between both files - """ - from openpilot.common.basedir import BASEDIR - - versions_json_path = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json") - sync_script_path = "python3 sunnypilot/sunnylink/tools/sync_torque_versions.py" - - # Load both files - with open(METADATA_PATH) as f: - metadata = json.load(f) - - with open(versions_json_path) as f: - versions = json.load(f) - - # Get TorqueControlTune metadata - torque_tune = metadata.get("TorqueControlTune") - if torque_tune is None: - pytest.fail(f"TorqueControlTune not found in params_metadata.json. Please run '{sync_script_path}' to sync.") - - if "options" not in torque_tune: - pytest.fail(f"TorqueControlTune must have options. Please run '{sync_script_path}' to sync.") - - options = torque_tune["options"] - if not isinstance(options, list): - pytest.fail(f"TorqueControlTune options must be a list. Please run '{sync_script_path}' to sync.") - - if len(options) == 0: - pytest.fail(f"TorqueControlTune must have at least one option. Please run '{sync_script_path}' to sync.") - - # Check that Default option exists - default_option = next((opt for opt in options if opt.get("value") == ""), None) - if default_option is None: - pytest.fail(f"TorqueControlTune must have a 'Default' option with empty string value. Please run '{sync_script_path}' to sync.") - - if default_option.get("label") != "Default": - pytest.fail(f"Default option must have label 'Default'. Please run '{sync_script_path}' to sync.") - - # Build expected options from versions.json - expected_version_keys = set(versions.keys()) - actual_version_keys = set() - - for option in options: - if option.get("value") == "": - continue # Skip the default option - - label = option.get("label") - value = option.get("value") - - # Check that this option corresponds to a version - if label not in versions: - pytest.fail(f"Option label '{label}' not found in latcontrol_torque_versions.json. Please run '{sync_script_path}' to sync.") - - # Check that the value matches the version number - expected_value = float(versions[label]["version"]) - if value != expected_value: - pytest.fail(f"Option '{label}' has value {value}, expected {expected_value}. Please run '{sync_script_path}' to sync.") - - actual_version_keys.add(label) - - # Check that all versions are represented - missing_versions = expected_version_keys - actual_version_keys - if missing_versions: - pytest.fail(f"The following versions are missing from TorqueControlTune options: {missing_versions}. " + - f"Please run '{sync_script_path}' to sync.") - - extra_versions = actual_version_keys - expected_version_keys - if extra_versions: - pytest.fail("The following versions in TorqueControlTune options are not in latcontrol_torque_versions.json: " + - f"{extra_versions}. Please run '{sync_script_path}' to sync.") diff --git a/sunnypilot/sunnylink/tools/update_params_metadata.py b/sunnypilot/sunnylink/tools/update_params_metadata.py deleted file mode 100755 index ea3765420d..0000000000 --- a/sunnypilot/sunnylink/tools/update_params_metadata.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -""" -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 json -import os - -from openpilot.common.basedir import BASEDIR -from openpilot.common.params import Params -from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES - -METADATA_PATH = os.path.join(os.path.dirname(__file__), "../params_metadata.json") -TORQUE_VERSIONS_JSON = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json") - - -def main(): - params = Params() - all_keys = params.all_keys() - - if os.path.exists(METADATA_PATH): - with open(METADATA_PATH) as f: - try: - data = json.load(f) - except json.JSONDecodeError: - data = {} - else: - data = {} - - # Add new keys - for key in all_keys: - key_str = key.decode("utf-8") - if key_str not in data: - print(f"Adding new key: {key_str}") - data[key_str] = { - "title": key_str, - "description": "", - } - - # Remove deleted keys - # keys_to_remove = [k for k in data.keys() if k.encode("utf-8") not in all_keys] - # for k in keys_to_remove: - # print(f"Removing deleted key: {k}") - # del data[k] - - # Sort keys - sorted_data = dict(sorted(data.items())) - - with open(METADATA_PATH, "w") as f: - json.dump(sorted_data, f, indent=2) - f.write("\n") - - print(f"Updated {METADATA_PATH}") - - # update onroad screen brightness params - update_onroad_brightness_param() - - # update onroad screen brightness timer params - update_onroad_brightness_timer_param() - - # update torque versions param - update_torque_versions_param() - - -def update_onroad_brightness_param(): - try: - with open(METADATA_PATH) as f: - params_metadata = json.load(f) - if "OnroadScreenOffBrightness" in params_metadata: - options = [ - {"value": 0, "label": "Auto (Default)"}, - {"value": 1, "label": "Auto (Dark)"}, - {"value": 2, "label": "Screen Off"}, - ] - for i in range(3, 23): - options.append({"value": i, "label": f"{(i - 2) * 5} %"}) - params_metadata["OnroadScreenOffBrightness"]["options"] = options - with open(METADATA_PATH, 'w') as f: - json.dump(params_metadata, f, indent=2) - f.write('\n') - print(f"Updated OnroadScreenOffBrightness options in params_metadata.json with {len(options)} options.") - except Exception as e: - print(f"Failed to update OnroadScreenOffBrightness versions in params_metadata.json: {e}") - - -def update_onroad_brightness_timer_param(): - try: - with open(METADATA_PATH) as f: - params_metadata = json.load(f) - if "OnroadScreenOffTimer" in params_metadata: - options = [] - for _index, seconds in sorted(ONROAD_BRIGHTNESS_TIMER_VALUES.items()): - label = f"{seconds}s" if seconds < 60 else f"{seconds // 60}m" - options.append({"value": seconds, "label": label}) - params_metadata["OnroadScreenOffTimer"]["options"] = options - with open(METADATA_PATH, 'w') as f: - json.dump(params_metadata, f, indent=2) - f.write('\n') - print(f"Updated OnroadScreenOffTimer options in params_metadata.json with {len(options)} options.") - except Exception as e: - print(f"Failed to update OnroadScreenOffTimer options in params_metadata.json: {e}") - - -def update_torque_versions_param(): - with open(TORQUE_VERSIONS_JSON) as f: - current_versions = json.load(f) - - try: - with open(METADATA_PATH) as f: - params_metadata = json.load(f) - - options = [{"value": "", "label": "Default"}] - for version_key, version_data in current_versions.items(): - version_value = float(version_data["version"]) - options.append({"value": version_value, "label": str(version_key)}) - - if "TorqueControlTune" in params_metadata: - params_metadata["TorqueControlTune"]["options"] = options - - with open(METADATA_PATH, 'w') as f: - json.dump(params_metadata, f, indent=2) - f.write('\n') - - print(f"Updated TorqueControlTune options in params_metadata.json with {len(options)} options: \n{options}") - - except Exception as e: - print(f"Failed to update TorqueControlTune versions in params_metadata.json: {e}") - - -if __name__ == "__main__": - main() diff --git a/sunnypilot/system/hardware/c3/README.md b/sunnypilot/system/hardware/c3/README.md deleted file mode 100644 index f74a210191..0000000000 --- a/sunnypilot/system/hardware/c3/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# C3 specific hardware code - -`c3` is known as `tici` and comma three by comma. Not to confuse it with `c3x` which is known as `tizi`. \ No newline at end of file diff --git a/sunnypilot/system/hardware/c3/agnos.json b/sunnypilot/system/hardware/c3/agnos.json deleted file mode 100644 index 941a4956bf..0000000000 --- a/sunnypilot/system/hardware/c3/agnos.json +++ /dev/null @@ -1,84 +0,0 @@ -[ - { - "name": "xbl", - "url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz", - "hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", - "hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", - "size": 3282256, - "sparse": false, - "full_check": true, - "has_ab": true, - "ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89" - }, - { - "name": "xbl_config", - "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz", - "hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", - "hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", - "size": 98124, - "sparse": false, - "full_check": true, - "has_ab": true, - "ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f" - }, - { - "name": "abl", - "url": "https://commadist.azureedge.net/agnosupdate/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz", - "hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6", - "hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6", - "size": 274432, - "sparse": false, - "full_check": true, - "has_ab": true, - "ondevice_hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6" - }, - { - "name": "aop", - "url": "https://commadist.azureedge.net/agnosupdate/aop-21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9.img.xz", - "hash": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9", - "hash_raw": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9", - "size": 184364, - "sparse": false, - "full_check": true, - "has_ab": true, - "ondevice_hash": "c1be2f4aac5b3af49b904b027faec418d05efd7bd5144eb4fdfcba602bcf2180" - }, - { - "name": "devcfg", - "url": "https://commadist.azureedge.net/agnosupdate/devcfg-d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620.img.xz", - "hash": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620", - "hash_raw": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620", - "size": 40336, - "sparse": false, - "full_check": true, - "has_ab": true, - "ondevice_hash": "17b229668b20305ff8fa3cd5f94716a3aaa1e5bf9d1c24117eff7f2f81ae719f" - }, - { - "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz", - "hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", - "hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", - "size": 18515968, - "sparse": false, - "full_check": true, - "has_ab": true, - "ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0" - }, - { - "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img.xz", - "hash": "1468d50b7ad0fda0f04074755d21e786e3b1b6ca5dd5b17eb2608202025e6126", - "hash_raw": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087", - "size": 5368709120, - "sparse": true, - "full_check": false, - "has_ab": true, - "ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7", - "alt": { - "hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087", - "url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img", - "size": 5368709120 - } - } -] \ No newline at end of file diff --git a/sunnypilot/system/hardware/c3/launch_chffrplus.sh b/sunnypilot/system/hardware/c3/launch_chffrplus.sh deleted file mode 100755 index 45cc950537..0000000000 --- a/sunnypilot/system/hardware/c3/launch_chffrplus.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env bash - -SP_C3_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -DIR="$( cd "$SP_C3_DIR/../../../.." >/dev/null 2>&1 && pwd )" - -source "$SP_C3_DIR/launch_env.sh" - -function agnos_init { - # TODO: move this to agnos - sudo rm -f /data/etc/NetworkManager/system-connections/*.nmmeta - - # set success flag for current boot slot - sudo abctl --set_success - - # TODO: do this without udev in AGNOS - # udev does this, but sometimes we startup faster - sudo chgrp gpu /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 - sudo chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 - - - if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then - AGNOS_PY="$DIR/system/hardware/tici/agnos.py" - MANIFEST="$SP_C3_DIR/agnos.json" - if $AGNOS_PY --verify $MANIFEST; then - sudo reboot - fi - $DIR/system/hardware/tici/updater $AGNOS_PY $MANIFEST - fi -} - -function launch { - # Remove orphaned git lock if it exists on boot - [ -f "$DIR/.git/index.lock" ] && rm -f $DIR/.git/index.lock - - # Check to see if there's a valid overlay-based update available. Conditions - # are as follows: - # - # 1. The DIR init file has to exist, with a newer modtime than anything in - # the DIR Git repo. This checks for local development work or the user - # switching branches/forks, which should not be overwritten. - # 2. The FINALIZED consistent file has to exist, indicating there's an update - # that completed successfully and synced to disk. - - if [ -f "${DIR}/.overlay_init" ]; then - find ${DIR}/.git -newer ${DIR}/.overlay_init | grep -q '.' 2> /dev/null - if [ $? -eq 0 ]; then - echo "${DIR} has been modified, skipping overlay update installation" - else - if [ -f "${STAGING_ROOT}/finalized/.overlay_consistent" ]; then - if [ ! -d /data/safe_staging/old_openpilot ]; then - echo "Valid overlay update found, installing" - LAUNCHER_LOCATION="${BASH_SOURCE[0]}" - - mv $DIR /data/safe_staging/old_openpilot - mv "${STAGING_ROOT}/finalized" $DIR - cd $DIR - - echo "Restarting launch script ${LAUNCHER_LOCATION}" - unset AGNOS_VERSION - exec "${LAUNCHER_LOCATION}" - else - echo "openpilot backup found, not updating" - # TODO: restore backup? This means the updater didn't start after swapping - fi - fi - fi - fi - - # handle pythonpath - ln -sfn $(pwd) /data/pythonpath - export PYTHONPATH="$PWD" - - # hardware specific init - if [ -f /AGNOS ]; then - agnos_init - fi - - # write tmux scrollback to a file - tmux capture-pane -pq -S-1000 > /tmp/launch_log - - # start manager - cd $DIR/system/manager - if [ ! -f $DIR/prebuilt ]; then - ./build.py - fi - ./manager.py - - # if broken, keep on screen error - while true; do sleep 1; done -} - -launch diff --git a/sunnypilot/system/hardware/c3/launch_env.sh b/sunnypilot/system/hardware/c3/launch_env.sh deleted file mode 100755 index 4c011c6ac0..0000000000 --- a/sunnypilot/system/hardware/c3/launch_env.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -export OMP_NUM_THREADS=1 -export MKL_NUM_THREADS=1 -export NUMEXPR_NUM_THREADS=1 -export OPENBLAS_NUM_THREADS=1 -export VECLIB_MAXIMUM_THREADS=1 - -if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="12.8" -fi - -export STAGING_ROOT="/data/safe_staging" diff --git a/system/hardware/pc/hardware.py b/system/hardware/pc/hardware.py deleted file mode 100644 index f3d527429c..0000000000 --- a/system/hardware/pc/hardware.py +++ /dev/null @@ -1,12 +0,0 @@ -from cereal import log -from openpilot.system.hardware.base import HardwareBase - -NetworkType = log.DeviceState.NetworkType - - -class Pc(HardwareBase): - def get_device_type(self): - return "pc" - - def get_network_type(self): - return NetworkType.wifi diff --git a/system/hardware/tici/iwlist.py b/system/hardware/tici/iwlist.py deleted file mode 100644 index 1e7c428b40..0000000000 --- a/system/hardware/tici/iwlist.py +++ /dev/null @@ -1,35 +0,0 @@ -import subprocess - - -def scan(interface="wlan0"): - result = [] - try: - r = subprocess.check_output(["iwlist", interface, "scan"], encoding='utf8') - - mac = None - for line in r.split('\n'): - if "Address" in line: - # Based on the adapter eithere a percentage or dBm is returned - # Add previous network in case no dBm signal level was seen - if mac is not None: - result.append({"mac": mac}) - mac = None - - mac = line.split(' ')[-1] - elif "dBm" in line: - try: - level = line.split('Signal level=')[1] - rss = int(level.split(' ')[0]) - result.append({"mac": mac, "rss": rss}) - mac = None - except ValueError: - continue - - # Add last network if no dBm was found - if mac is not None: - result.append({"mac": mac}) - - return result - - except Exception: - return None diff --git a/system/hardware/tici/precise_power_measure.py b/system/hardware/tici/precise_power_measure.py deleted file mode 100755 index 52fe0850ab..0000000000 --- a/system/hardware/tici/precise_power_measure.py +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env python3 -import numpy as np -from openpilot.system.hardware.tici.power_monitor import sample_power - -if __name__ == '__main__': - print("measuring for 5 seconds") - for _ in range(3): - pwrs = sample_power() - print(f"mean {np.mean(pwrs):.2f} std {np.std(pwrs):.2f}") diff --git a/system/hardware/tici/tests/compare_casync_manifest.py b/system/hardware/tici/tests/compare_casync_manifest.py deleted file mode 100755 index 7de66d91d0..0000000000 --- a/system/hardware/tici/tests/compare_casync_manifest.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import collections -import multiprocessing -import os - -import requests -from tqdm import tqdm - -import openpilot.system.hardware.tici.casync as casync - - -def get_chunk_download_size(chunk): - sha = chunk.sha.hex() - path = os.path.join(remote_url, sha[:4], sha + ".cacnk") - if os.path.isfile(path): - return os.path.getsize(path) - else: - r = requests.head(path, timeout=10) - r.raise_for_status() - return int(r.headers['content-length']) - - -if __name__ == "__main__": - - parser = argparse.ArgumentParser(description='Compute overlap between two casync manifests') - parser.add_argument('frm') - parser.add_argument('to') - args = parser.parse_args() - - frm = casync.parse_caibx(args.frm) - to = casync.parse_caibx(args.to) - remote_url = args.to.replace('.caibx', '') - - most_common = collections.Counter(t.sha for t in to).most_common(1)[0][0] - - frm_dict = casync.build_chunk_dict(frm) - - # Get content-length for each chunk - with multiprocessing.Pool() as pool: - szs = list(tqdm(pool.imap(get_chunk_download_size, to), total=len(to))) - chunk_sizes = {t.sha: sz for (t, sz) in zip(to, szs, strict=True)} - - sources: dict[str, list[int]] = { - 'seed': [], - 'remote_uncompressed': [], - 'remote_compressed': [], - } - - for chunk in to: - # Assume most common chunk is the zero chunk - if chunk.sha == most_common: - continue - - if chunk.sha in frm_dict: - sources['seed'].append(chunk.length) - else: - sources['remote_uncompressed'].append(chunk.length) - sources['remote_compressed'].append(chunk_sizes[chunk.sha]) - - print() - print("Update statistics (excluding zeros)") - print() - print("Download only with no seed:") - print(f" Remote (uncompressed)\t\t{sum(sources['seed'] + sources['remote_uncompressed']) / 1000 / 1000:.2f} MB\tn = {len(to)}") - print(f" Remote (compressed download)\t{sum(chunk_sizes.values()) / 1000 / 1000:.2f} MB\tn = {len(to)}") - print() - print("Upgrade with seed partition:") - print(f" Seed (uncompressed)\t\t{sum(sources['seed']) / 1000 / 1000:.2f} MB\t\t\t\tn = {len(sources['seed'])}") - sz, n = sum(sources['remote_uncompressed']), len(sources['remote_uncompressed']) - print(f" Remote (uncompressed)\t\t{sz / 1000 / 1000:.2f} MB\t(avg {sz / 1000 / 1000 / n:4f} MB)\tn = {n}") - sz, n = sum(sources['remote_compressed']), len(sources['remote_compressed']) - print(f" Remote (compressed download)\t{sz / 1000 / 1000:.2f} MB\t(avg {sz / 1000 / 1000 / n:4f} MB)\tn = {n}") diff --git a/system/loggerd/encoder/jpeg_encoder.cc b/system/loggerd/encoder/jpeg_encoder.cc deleted file mode 100644 index 6bb946157c..0000000000 --- a/system/loggerd/encoder/jpeg_encoder.cc +++ /dev/null @@ -1,105 +0,0 @@ -#include "system/loggerd/encoder/jpeg_encoder.h" - -#include -#include - -JpegEncoder::JpegEncoder(const std::string &pusblish_name, int width, int height) - : publish_name(pusblish_name), thumbnail_width(width), thumbnail_height(height) { - yuv_buffer.resize((thumbnail_width * ((thumbnail_height + 15) & ~15) * 3) / 2); - pm = std::make_unique(std::vector{pusblish_name.c_str()}); -} - -JpegEncoder::~JpegEncoder() { - if (out_buffer) { - free(out_buffer); - } -} - -void JpegEncoder::pushThumbnail(VisionBuf *buf, const VisionIpcBufExtra &extra) { - generateThumbnail(buf->y, buf->uv, buf->width, buf->height, buf->stride); - - MessageBuilder msg; - auto thumbnaild = msg.initEvent().initThumbnail(); - thumbnaild.setFrameId(extra.frame_id); - thumbnaild.setTimestampEof(extra.timestamp_eof); - thumbnaild.setThumbnail({out_buffer, out_size}); - - pm->send(publish_name.c_str(), msg); -} - -void JpegEncoder::generateThumbnail(const uint8_t *y_addr, const uint8_t *uv_addr, int width, int height, int stride) { - int downscale = width / thumbnail_width; - assert(downscale * thumbnail_height == height); - - // make the buffer big enough. jpeg_write_raw_data requires 16-pixels aligned height to be used. - uint8_t *y_plane = yuv_buffer.data(); - uint8_t *u_plane = y_plane + thumbnail_width * thumbnail_height; - uint8_t *v_plane = u_plane + (thumbnail_width * thumbnail_height) / 4; - { - // subsampled conversion from nv12 to yuv - for (int hy = 0; hy < thumbnail_height / 2; hy++) { - for (int hx = 0; hx < thumbnail_width / 2; hx++) { - int ix = hx * downscale + (downscale - 1) / 2; - int iy = hy * downscale + (downscale - 1) / 2; - y_plane[(hy * 2 + 0) * thumbnail_width + (hx * 2 + 0)] = y_addr[(iy * 2 + 0) * stride + ix * 2 + 0]; - y_plane[(hy * 2 + 0) * thumbnail_width + (hx * 2 + 1)] = y_addr[(iy * 2 + 0) * stride + ix * 2 + 1]; - y_plane[(hy * 2 + 1) * thumbnail_width + (hx * 2 + 0)] = y_addr[(iy * 2 + 1) * stride + ix * 2 + 0]; - y_plane[(hy * 2 + 1) * thumbnail_width + (hx * 2 + 1)] = y_addr[(iy * 2 + 1) * stride + ix * 2 + 1]; - u_plane[hy * thumbnail_width / 2 + hx] = uv_addr[iy * stride + ix * 2 + 0]; - v_plane[hy * thumbnail_width / 2 + hx] = uv_addr[iy * stride + ix * 2 + 1]; - } - } - } - - compressToJpeg(y_plane, u_plane, v_plane); -} - -void JpegEncoder::compressToJpeg(uint8_t *y_plane, uint8_t *u_plane, uint8_t *v_plane) { - struct jpeg_compress_struct cinfo; - struct jpeg_error_mgr jerr; - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_compress(&cinfo); - - if (out_buffer) { - free(out_buffer); - out_buffer = nullptr; - out_size = 0; - } - jpeg_mem_dest(&cinfo, &out_buffer, &out_size); - - cinfo.image_width = thumbnail_width; - cinfo.image_height = thumbnail_height; - cinfo.input_components = 3; - - jpeg_set_defaults(&cinfo); - jpeg_set_colorspace(&cinfo, JCS_YCbCr); - // configure sampling factors for yuv420. - cinfo.comp_info[0].h_samp_factor = 2; // Y - cinfo.comp_info[0].v_samp_factor = 2; - cinfo.comp_info[1].h_samp_factor = 1; // U - cinfo.comp_info[1].v_samp_factor = 1; - cinfo.comp_info[2].h_samp_factor = 1; // V - cinfo.comp_info[2].v_samp_factor = 1; - cinfo.raw_data_in = TRUE; - - jpeg_set_quality(&cinfo, 50, TRUE); - jpeg_start_compress(&cinfo, TRUE); - - JSAMPROW y[16], u[8], v[8]; - JSAMPARRAY planes[3]{y, u, v}; - - for (int line = 0; line < cinfo.image_height; line += 16) { - for (int i = 0; i < 16; ++i) { - y[i] = y_plane + (line + i) * cinfo.image_width; - if (i % 2 == 0) { - int offset = (cinfo.image_width / 2) * ((i + line) / 2); - u[i / 2] = u_plane + offset; - v[i / 2] = v_plane + offset; - } - } - jpeg_write_raw_data(&cinfo, planes, 16); - } - - jpeg_finish_compress(&cinfo); - jpeg_destroy_compress(&cinfo); -} diff --git a/system/loggerd/xattr_cache.py b/system/loggerd/xattr_cache.py deleted file mode 100644 index 39bb172059..0000000000 --- a/system/loggerd/xattr_cache.py +++ /dev/null @@ -1,23 +0,0 @@ -import errno - -import xattr - -_cached_attributes: dict[tuple, bytes | None] = {} - -def getxattr(path: str, attr_name: str) -> bytes | None: - key = (path, attr_name) - if key not in _cached_attributes: - try: - response = xattr.getxattr(path, attr_name) - except OSError as e: - # ENODATA (Linux) or ENOATTR (macOS) means attribute hasn't been set - if e.errno == errno.ENODATA or (hasattr(errno, 'ENOATTR') and e.errno == errno.ENOATTR): - response = None - else: - raise - _cached_attributes[key] = response - return _cached_attributes[key] - -def setxattr(path: str, attr_name: str, attr_value: bytes) -> None: - _cached_attributes.pop((path, attr_name), None) - xattr.setxattr(path, attr_name, attr_value) diff --git a/system/manager/process_config.py b/system/manager/process_config.py deleted file mode 100644 index 2b8def95b5..0000000000 --- a/system/manager/process_config.py +++ /dev/null @@ -1,204 +0,0 @@ -import os -import operator -import platform - -from cereal import car, custom -from openpilot.common.params import Params -from openpilot.system.hardware import PC, TICI -from openpilot.system.manager.process import PythonProcess, NativeProcess, DaemonProcess -from openpilot.system.hardware.hw import Paths - -from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH - -from openpilot.sunnypilot.models.helpers import get_active_model_runner -from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, use_sunnylink_uploader - -WEBCAM = os.getenv("USE_WEBCAM") is not None - -def driverview(started: bool, params: Params, CP: car.CarParams) -> bool: - return started or params.get_bool("IsDriverViewEnabled") - -def notcar(started: bool, params: Params, CP: car.CarParams) -> bool: - return started and CP.notCar - -def iscar(started: bool, params: Params, CP: car.CarParams) -> bool: - return started and not CP.notCar - -def logging(started: bool, params: Params, CP: car.CarParams) -> bool: - run = (not CP.notCar) or not params.get_bool("DisableLogging") - return started and run - -def ublox_available() -> bool: - return os.path.exists('/dev/ttyHS0') and not os.path.exists('/persist/comma/use-quectel-gps') - -def ublox(started: bool, params: Params, CP: car.CarParams) -> bool: - use_ublox = ublox_available() - if use_ublox != params.get_bool("UbloxAvailable"): - params.put_bool("UbloxAvailable", use_ublox, block=True) - return started and use_ublox - -def joystick(started: bool, params: Params, CP: car.CarParams) -> bool: - return started and params.get_bool("JoystickDebugMode") - -def not_joystick(started: bool, params: Params, CP: car.CarParams) -> bool: - return started and not params.get_bool("JoystickDebugMode") - -def long_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool: - return started and params.get_bool("LongitudinalManeuverMode") - -def lat_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool: - return started and params.get_bool("LateralManeuverMode") - -def not_long_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool: - return started and not params.get_bool("LongitudinalManeuverMode") - -def qcomgps(started: bool, params: Params, CP: car.CarParams) -> bool: - return started and not ublox_available() - -def always_run(started: bool, params: Params, CP: car.CarParams) -> bool: - return True - -def only_onroad(started: bool, params: Params, CP: car.CarParams) -> bool: - return started - -def only_offroad(started: bool, params: Params, CP: car.CarParams) -> bool: - return not started - -def use_github_runner(started, params, CP: car.CarParams) -> bool: - return not PC and params.get_bool("EnableGithubRunner") and ( - not params.get_bool("NetworkMetered") and not params.get_bool("GithubRunnerSufficientVoltage")) - -def use_copyparty(started, params, CP: car.CarParams) -> bool: - return bool(params.get_bool("EnableCopyparty")) - -def sunnylink_ready_shim(started, params, CP: car.CarParams) -> bool: - """Shim for sunnylink_ready to match the process manager signature.""" - return sunnylink_ready(params) - -def sunnylink_need_register_shim(started, params, CP: car.CarParams) -> bool: - """Shim for sunnylink_need_register to match the process manager signature.""" - return sunnylink_need_register(params) - -def use_sunnylink_uploader_shim(started, params, CP: car.CarParams) -> bool: - """Shim for use_sunnylink_uploader to match the process manager signature.""" - return use_sunnylink_uploader(params) - -def is_tinygrad_model(started, params, CP: car.CarParams) -> bool: - """Check if the active model runner is SNPE.""" - return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.tinygrad) - -def is_stock_model(started, params, CP: car.CarParams) -> bool: - """Check if the active model runner is stock.""" - return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.stock) - -def mapd_ready(started: bool, params: Params, CP: car.CarParams) -> bool: - return bool(os.path.exists(Paths.mapd_root())) - -def uploader_ready(started: bool, params: Params, CP: car.CarParams) -> bool: - if not params.get_bool("OnroadUploads"): - return only_offroad(started, params, CP) - - return always_run(started, params, CP) - -def or_(*fns): - return lambda *args: operator.or_(*(fn(*args) for fn in fns)) - -def and_(*fns): - return lambda *args: operator.and_(*(fn(*args) for fn in fns)) - -procs = [ - DaemonProcess("manage_athenad", "system.athena.manage_athenad", "AthenadPid"), - - NativeProcess("loggerd", "system/loggerd", ["./loggerd"], logging), - NativeProcess("encoderd", "system/loggerd", ["./encoderd"], only_onroad), - NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar), - PythonProcess("logmessaged", "system.logmessaged", always_run), - - NativeProcess("camerad", "system/camerad", ["./camerad"], driverview, enabled=not WEBCAM), - PythonProcess("webcamerad", "tools.webcam.camerad", driverview, enabled=WEBCAM), - PythonProcess("proclogd", "system.proclogd", only_onroad, enabled=platform.system() != "Darwin"), - PythonProcess("journald", "system.journald", only_onroad, platform.system() != "Darwin"), - PythonProcess("micd", "system.micd", iscar), - PythonProcess("timed", "system.timed", always_run, enabled=not PC), - - PythonProcess("modeld", "selfdrive.modeld.modeld", and_(only_onroad, is_stock_model)), - PythonProcess("dmonitoringmodeld", "selfdrive.modeld.dmonitoringmodeld", driverview, enabled=(WEBCAM or not PC)), - - PythonProcess("sensord", "system.sensord.sensord", only_onroad, enabled=not PC), - PythonProcess("ui", "selfdrive.ui.ui", always_run, restart_if_crash=True), - PythonProcess("soundd", "selfdrive.ui.soundd", driverview), - PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad), - NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False), - PythonProcess("calibrationd", "selfdrive.locationd.calibrationd", only_onroad), - PythonProcess("torqued", "selfdrive.locationd.torqued", only_onroad), - PythonProcess("controlsd", "selfdrive.controls.controlsd", and_(not_joystick, iscar)), - PythonProcess("joystickd", "tools.joystick.joystickd", or_(joystick, notcar)), - PythonProcess("selfdrived", "selfdrive.selfdrived.selfdrived", only_onroad), - PythonProcess("card", "selfdrive.car.card", only_onroad), - PythonProcess("deleter", "system.loggerd.deleter", always_run), - PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driverview, enabled=(WEBCAM or not PC)), - PythonProcess("qcomgpsd", "system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI), - PythonProcess("pandad", "selfdrive.pandad.pandad", always_run), - PythonProcess("paramsd", "selfdrive.locationd.paramsd", only_onroad), - PythonProcess("lagd", "selfdrive.locationd.lagd", only_onroad), - PythonProcess("ubloxd", "system.ubloxd.ubloxd", ublox, enabled=TICI), - PythonProcess("pigeond", "system.ubloxd.pigeond", ublox, enabled=TICI), - PythonProcess("plannerd", "selfdrive.controls.plannerd", not_long_maneuver), - PythonProcess("maneuversd", "tools.longitudinal_maneuvers.maneuversd", long_maneuver), - PythonProcess("lateral_maneuversd", "tools.lateral_maneuvers.lateral_maneuversd", lat_maneuver), - PythonProcess("radard", "selfdrive.controls.radard", only_onroad), - PythonProcess("hardwared", "system.hardware.hardwared", always_run), - PythonProcess("modem", "system.hardware.tici.modem", always_run, enabled=TICI), - PythonProcess("tombstoned", "system.tombstoned", always_run, enabled=not PC), - PythonProcess("updated", "system.updated.updated", only_offroad, enabled=not PC), - PythonProcess("uploader", "system.loggerd.uploader", uploader_ready), - PythonProcess("statsd", "system.statsd", always_run), - PythonProcess("feedbackd", "selfdrive.ui.feedback.feedbackd", only_onroad), - - # debug procs - NativeProcess("bridge", "cereal/messaging", ["./bridge"], notcar), - PythonProcess("webrtcd", "system.webrtc.webrtcd", notcar), - PythonProcess("webjoystick", "tools.bodyteleop.web", notcar), - PythonProcess("joystick", "tools.joystick.joystick_control", and_(joystick, iscar)), - - # sunnylink <3 - DaemonProcess("manage_sunnylinkd", "sunnypilot.sunnylink.athena.manage_sunnylinkd", "SunnylinkdPid"), - PythonProcess("sunnylink_registration_manager", "sunnypilot.sunnylink.registration_manager", sunnylink_need_register_shim), - PythonProcess("statsd_sp", "sunnypilot.sunnylink.statsd", and_(always_run, sunnylink_ready_shim)), -] - -# sunnypilot -procs += [ - # Models - PythonProcess("models_manager", "sunnypilot.models.manager", only_offroad), - NativeProcess("modeld_tinygrad", "sunnypilot/modeld_v2", ["./modeld"], and_(only_onroad, is_tinygrad_model)), - - # Backup - PythonProcess("backup_manager", "sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)), - - # mapd - NativeProcess("mapd", Paths.mapd_root(), ["bash", "-c", f"{MAPD_PATH} > /dev/null 2>&1"], mapd_ready), - PythonProcess("mapd_manager", "sunnypilot.mapd.mapd_manager", always_run), - - # locationd - NativeProcess("locationd_llk", "sunnypilot/selfdrive/locationd", ["./locationd"], only_onroad), -] - -if os.path.exists("./github_runner.sh"): - procs += [NativeProcess("github_runner_start", "system/manager", ["./github_runner.sh", "start"], and_(only_offroad, use_github_runner), sigkill=False)] - -if os.path.exists("../../sunnypilot/sunnylink/uploader.py"): - procs += [PythonProcess("sunnylink_uploader", "sunnypilot.sunnylink.uploader", use_sunnylink_uploader_shim)] - -if os.path.exists("../../third_party/copyparty/copyparty-sfx.py"): - sunnypilot_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) - copyparty_args = [f"-v{Paths.crash_log_root()}:/swaglogs:r"] - copyparty_args += [f"-v{Paths.log_root()}:/routes:r"] - copyparty_args += [f"-v{Paths.model_root()}:/models:rw"] - copyparty_args += [f"-v{sunnypilot_root}:/sunnypilot:rw"] - copyparty_args += ["-p8080"] - copyparty_args += ["-z"] - copyparty_args += ["-q"] - procs += [NativeProcess("copyparty-sfx", "third_party/copyparty", ["./copyparty-sfx.py", *copyparty_args], and_(only_offroad, use_copyparty))] - -managed_processes = {p.name: p for p in procs} diff --git a/system/updated/casync/casync.py b/system/updated/casync/casync.py deleted file mode 100755 index 2bd46a1ffb..0000000000 --- a/system/updated/casync/casync.py +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/env python3 -import io -import lzma -import os -import pathlib -import struct -import sys -import time -from abc import ABC, abstractmethod -from collections import defaultdict, namedtuple -from collections.abc import Callable -from typing import IO - -import requests -from Crypto.Hash import SHA512 -from openpilot.system.updated.casync import tar -from openpilot.system.updated.casync.common import create_casync_tar_package - -CA_FORMAT_INDEX = 0x96824d9c7b129ff9 -CA_FORMAT_TABLE = 0xe75b9e112f17417d -CA_FORMAT_TABLE_TAIL_MARKER = 0xe75b9e112f17417 -FLAGS = 0xb000000000000000 - -CA_HEADER_LEN = 48 -CA_TABLE_HEADER_LEN = 16 -CA_TABLE_ENTRY_LEN = 40 -CA_TABLE_MIN_LEN = CA_TABLE_HEADER_LEN + CA_TABLE_ENTRY_LEN - -CHUNK_DOWNLOAD_TIMEOUT = 60 -CHUNK_DOWNLOAD_RETRIES = 3 - -CAIBX_DOWNLOAD_TIMEOUT = 120 - -Chunk = namedtuple('Chunk', ['sha', 'offset', 'length']) -ChunkDict = dict[bytes, Chunk] - - -class ChunkReader(ABC): - @abstractmethod - def read(self, chunk: Chunk) -> bytes: - ... - - -class BinaryChunkReader(ChunkReader): - """Reads chunks from a local file""" - def __init__(self, file_like: IO[bytes]) -> None: - super().__init__() - self.f = file_like - - def read(self, chunk: Chunk) -> bytes: - self.f.seek(chunk.offset) - return self.f.read(chunk.length) - - -class FileChunkReader(BinaryChunkReader): - def __init__(self, path: str) -> None: - super().__init__(open(path, 'rb')) - - def __del__(self): - self.f.close() - - -class RemoteChunkReader(ChunkReader): - """Reads lzma compressed chunks from a remote store""" - - def __init__(self, url: str) -> None: - super().__init__() - self.url = url - self.session = requests.Session() - - def read(self, chunk: Chunk) -> bytes: - sha_hex = chunk.sha.hex() - url = os.path.join(self.url, sha_hex[:4], sha_hex + ".cacnk") - - if os.path.isfile(url): - with open(url, 'rb') as f: - contents = f.read() - else: - for i in range(CHUNK_DOWNLOAD_RETRIES): - try: - resp = self.session.get(url, timeout=CHUNK_DOWNLOAD_TIMEOUT) - break - except Exception: - if i == CHUNK_DOWNLOAD_RETRIES - 1: - raise - time.sleep(CHUNK_DOWNLOAD_TIMEOUT) - - resp.raise_for_status() - contents = resp.content - - decompressor = lzma.LZMADecompressor(format=lzma.FORMAT_AUTO) - return decompressor.decompress(contents) - - -class DirectoryTarChunkReader(BinaryChunkReader): - """creates a tar archive of a directory and reads chunks from it""" - - def __init__(self, path: str, cache_file: str) -> None: - create_casync_tar_package(pathlib.Path(path), pathlib.Path(cache_file)) - - self.f = open(cache_file, "rb") - super().__init__(self.f) - - def __del__(self): - self.f.close() - os.unlink(self.f.name) - - -def parse_caibx(caibx_path: str) -> list[Chunk]: - """Parses the chunks from a caibx file. Can handle both local and remote files. - Returns a list of chunks with hash, offset and length""" - caibx: io.BufferedIOBase - if os.path.isfile(caibx_path): - caibx = open(caibx_path, 'rb') - else: - resp = requests.get(caibx_path, timeout=CAIBX_DOWNLOAD_TIMEOUT) - resp.raise_for_status() - caibx = io.BytesIO(resp.content) - - caibx.seek(0, os.SEEK_END) - caibx_len = caibx.tell() - caibx.seek(0, os.SEEK_SET) - - # Parse header - length, magic, flags, min_size, _, max_size = struct.unpack("= min_size - - chunks.append(Chunk(sha, offset, length)) - offset = new_offset - - caibx.close() - return chunks - - -def build_chunk_dict(chunks: list[Chunk]) -> ChunkDict: - """Turn a list of chunks into a dict for faster lookups based on hash. - Keep first chunk since it's more likely to be already downloaded.""" - r = {} - for c in chunks: - if c.sha not in r: - r[c.sha] = c - return r - - -def extract(target: list[Chunk], - sources: list[tuple[str, ChunkReader, ChunkDict]], - out_path: str, - progress: Callable[[int], None] | None = None): - stats: dict[str, int] = defaultdict(int) - - mode = 'rb+' if os.path.exists(out_path) else 'wb' - with open(out_path, mode) as out: - for cur_chunk in target: - - # Find source for desired chunk - for name, chunk_reader, store_chunks in sources: - if cur_chunk.sha in store_chunks: - bts = chunk_reader.read(store_chunks[cur_chunk.sha]) - - # Check length - if len(bts) != cur_chunk.length: - continue - - # Check hash - if SHA512.new(bts, truncate="256").digest() != cur_chunk.sha: - continue - - # Write to output - out.seek(cur_chunk.offset) - out.write(bts) - - stats[name] += cur_chunk.length - - if progress is not None: - progress(sum(stats.values())) - - break - else: - raise RuntimeError("Desired chunk not found in provided stores") - - return stats - - -def extract_directory(target: list[Chunk], - sources: list[tuple[str, ChunkReader, ChunkDict]], - out_path: str, - tmp_file: str, - progress: Callable[[int], None] | None = None): - """extract a directory stored as a casync tar archive""" - - stats = extract(target, sources, tmp_file, progress) - - with open(tmp_file, "rb") as f: - tar.extract_tar_archive(f, pathlib.Path(out_path)) - - return stats - - -def print_stats(stats: dict[str, int]): - total_bytes = sum(stats.values()) - print(f"Total size: {total_bytes / 1024 / 1024:.2f} MB") - for name, total in stats.items(): - print(f" {name}: {total / 1024 / 1024:.2f} MB ({total / total_bytes * 100:.1f}%)") - - -def extract_simple(caibx_path, out_path, store_path): - # (name, callback, chunks) - target = parse_caibx(caibx_path) - sources = [ - # (store_path, RemoteChunkReader(store_path), build_chunk_dict(target)), - (store_path, FileChunkReader(store_path), build_chunk_dict(target)), - ] - - return extract(target, sources, out_path) - - -if __name__ == "__main__": - caibx = sys.argv[1] - out = sys.argv[2] - store = sys.argv[3] - - stats = extract_simple(caibx, out, store) - print_stats(stats) diff --git a/system/updated/casync/common.py b/system/updated/casync/common.py deleted file mode 100644 index 6979f5cb06..0000000000 --- a/system/updated/casync/common.py +++ /dev/null @@ -1,61 +0,0 @@ -import dataclasses -import json -import pathlib -import subprocess - -from openpilot.system.version import BUILD_METADATA_FILENAME, BuildMetadata -from openpilot.system.updated.casync import tar - - -CASYNC_ARGS = ["--with=symlinks", "--with=permissions", "--compression=xz", "--chunk-size=16M"] -CASYNC_FILES = [BUILD_METADATA_FILENAME] - - -def run(cmd): - return subprocess.check_output(cmd) - - -def get_exclude_set(path) -> set[str]: - exclude_set = set(CASYNC_FILES) - - for file in path.rglob("*"): - if file.is_file() or file.is_symlink(): - - while file.resolve() != path.resolve(): - exclude_set.add(str(file.relative_to(path))) - - file = file.parent - - return exclude_set - - -def create_build_metadata_file(path: pathlib.Path, build_metadata: BuildMetadata): - with open(path / BUILD_METADATA_FILENAME, "w") as f: - build_metadata_dict = dataclasses.asdict(build_metadata) - build_metadata_dict["openpilot"].pop("is_dirty") # this is determined at runtime - build_metadata_dict.pop("channel") # channel is unrelated to the build itself - f.write(json.dumps(build_metadata_dict)) - - -def is_not_git(path: pathlib.Path) -> bool: - return ".git" not in path.parts - - -def create_casync_tar_package(target_dir: pathlib.Path, output_path: pathlib.Path): - tar.create_tar_archive(output_path, target_dir, is_not_git) - - -def create_casync_from_file(file: pathlib.Path, output_dir: pathlib.Path, caibx_name: str): - caibx_file = output_dir / f"{caibx_name}.caibx" - run(["casync", "make", *CASYNC_ARGS, caibx_file, str(file)]) - - return caibx_file - - -def create_casync_release(target_dir: pathlib.Path, output_dir: pathlib.Path, caibx_name: str): - tar_file = output_dir / f"{caibx_name}.tar" - create_casync_tar_package(target_dir, tar_file) - caibx_file = create_casync_from_file(tar_file, output_dir, caibx_name) - tar_file.unlink() - digest = run(["casync", "digest", *CASYNC_ARGS, target_dir]).decode("utf-8").strip() - return digest, caibx_file diff --git a/system/updated/casync/tar.py b/system/updated/casync/tar.py deleted file mode 100644 index a5a8238bba..0000000000 --- a/system/updated/casync/tar.py +++ /dev/null @@ -1,39 +0,0 @@ -import pathlib -import tarfile -from typing import IO -from collections.abc import Callable - - -def include_default(_) -> bool: - return True - - -def create_tar_archive(filename: pathlib.Path, directory: pathlib.Path, include: Callable[[pathlib.Path], bool] = include_default): - """Creates a tar archive of a directory""" - - with tarfile.open(filename, 'w') as tar: - for file in sorted(directory.rglob("*"), key=lambda f: f.stat().st_size if f.is_file() else 0, reverse=True): - if not include(file): - continue - relative_path = str(file.relative_to(directory)) - if file.is_symlink(): - info = tarfile.TarInfo(relative_path) - info.type = tarfile.SYMTYPE - info.linkpath = str(file.readlink()) - tar.addfile(info) - - elif file.is_file(): - info = tarfile.TarInfo(relative_path) - info.size = file.stat().st_size - info.type = tarfile.REGTYPE - info.mode = file.stat().st_mode - with file.open('rb') as f: - tar.addfile(info, f) - - -def extract_tar_archive(fh: IO[bytes], directory: pathlib.Path): - """Extracts a tar archive to a directory""" - - tar = tarfile.open(fileobj=fh, mode='r') - tar.extractall(str(directory), filter=lambda info, path: info) - tar.close() diff --git a/system/updated/casync/tests/test_casync.py b/system/updated/casync/tests/test_casync.py deleted file mode 100644 index bc171e7432..0000000000 --- a/system/updated/casync/tests/test_casync.py +++ /dev/null @@ -1,264 +0,0 @@ -import pytest -import os -import pathlib -import tempfile -import subprocess - -from openpilot.system.updated.casync import casync -from openpilot.system.updated.casync import tar - -# dd if=/dev/zero of=/tmp/img.raw bs=1M count=2 -# sudo losetup -f /tmp/img.raw -# losetup -a | grep img.raw -LOOPBACK = os.environ.get('LOOPBACK', None) - - -@pytest.mark.skip("not used yet") -class TestCasync: - @classmethod - def setup_class(cls): - cls.tmpdir = tempfile.TemporaryDirectory() - - # Build example contents - chunk_a = [i % 256 for i in range(1024)] * 512 - chunk_b = [(256 - i) % 256 for i in range(1024)] * 512 - zeroes = [0] * (1024 * 128) - contents = chunk_a + chunk_b + zeroes + chunk_a - - cls.contents = bytes(contents) - - # Write to file - cls.orig_fn = os.path.join(cls.tmpdir.name, 'orig.bin') - with open(cls.orig_fn, 'wb') as f: - f.write(cls.contents) - - # Create casync files - cls.manifest_fn = os.path.join(cls.tmpdir.name, 'orig.caibx') - cls.store_fn = os.path.join(cls.tmpdir.name, 'store') - subprocess.check_output(["casync", "make", "--compression=xz", "--store", cls.store_fn, cls.manifest_fn, cls.orig_fn]) - - target = casync.parse_caibx(cls.manifest_fn) - hashes = [c.sha.hex() for c in target] - - # Ensure we have chunk reuse - assert len(hashes) > len(set(hashes)) - - def setup_method(self): - # Clear target_lo - if LOOPBACK is not None: - self.target_lo = LOOPBACK - with open(self.target_lo, 'wb') as f: - f.write(b"0" * len(self.contents)) - - self.target_fn = os.path.join(self.tmpdir.name, next(tempfile._get_candidate_names())) - self.seed_fn = os.path.join(self.tmpdir.name, next(tempfile._get_candidate_names())) - - def teardown_method(self): - for fn in [self.target_fn, self.seed_fn]: - try: - os.unlink(fn) - except FileNotFoundError: - pass - - def test_simple_extract(self): - target = casync.parse_caibx(self.manifest_fn) - - sources = [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))] - stats = casync.extract(target, sources, self.target_fn) - - with open(self.target_fn, 'rb') as target_f: - assert target_f.read() == self.contents - - assert stats['remote'] == len(self.contents) - - def test_seed(self): - target = casync.parse_caibx(self.manifest_fn) - - # Populate seed with half of the target contents - with open(self.seed_fn, 'wb') as seed_f: - seed_f.write(self.contents[:len(self.contents) // 2]) - - sources = [('seed', casync.FileChunkReader(self.seed_fn), casync.build_chunk_dict(target))] - sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))] - stats = casync.extract(target, sources, self.target_fn) - - with open(self.target_fn, 'rb') as target_f: - assert target_f.read() == self.contents - - assert stats['seed'] > 0 - assert stats['remote'] < len(self.contents) - - def test_already_done(self): - """Test that an already flashed target doesn't download any chunks""" - target = casync.parse_caibx(self.manifest_fn) - - with open(self.target_fn, 'wb') as f: - f.write(self.contents) - - sources = [('target', casync.FileChunkReader(self.target_fn), casync.build_chunk_dict(target))] - sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))] - - stats = casync.extract(target, sources, self.target_fn) - - with open(self.target_fn, 'rb') as f: - assert f.read() == self.contents - - assert stats['target'] == len(self.contents) - - def test_chunk_reuse(self): - """Test that chunks that are reused are only downloaded once""" - target = casync.parse_caibx(self.manifest_fn) - - # Ensure target exists - with open(self.target_fn, 'wb'): - pass - - sources = [('target', casync.FileChunkReader(self.target_fn), casync.build_chunk_dict(target))] - sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))] - - stats = casync.extract(target, sources, self.target_fn) - - with open(self.target_fn, 'rb') as f: - assert f.read() == self.contents - - assert stats['remote'] < len(self.contents) - - @pytest.mark.skipif(not LOOPBACK, reason="requires loopback device") - def test_lo_simple_extract(self): - target = casync.parse_caibx(self.manifest_fn) - sources = [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))] - - stats = casync.extract(target, sources, self.target_lo) - - with open(self.target_lo, 'rb') as target_f: - assert target_f.read(len(self.contents)) == self.contents - - assert stats['remote'] == len(self.contents) - - @pytest.mark.skipif(not LOOPBACK, reason="requires loopback device") - def test_lo_chunk_reuse(self): - """Test that chunks that are reused are only downloaded once""" - target = casync.parse_caibx(self.manifest_fn) - - sources = [('target', casync.FileChunkReader(self.target_lo), casync.build_chunk_dict(target))] - sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))] - - stats = casync.extract(target, sources, self.target_lo) - - with open(self.target_lo, 'rb') as f: - assert f.read(len(self.contents)) == self.contents - - assert stats['remote'] < len(self.contents) - - -@pytest.mark.skip("not used yet") -class TestCasyncDirectory: - """Tests extracting a directory stored as a casync tar archive""" - - NUM_FILES = 16 - - @classmethod - def setup_cache(cls, directory, files=None): - if files is None: - files = range(cls.NUM_FILES) - - chunk_a = [i % 256 for i in range(1024)] * 512 - chunk_b = [(256 - i) % 256 for i in range(1024)] * 512 - zeroes = [0] * (1024 * 128) - cls.contents = chunk_a + chunk_b + zeroes + chunk_a - cls.contents = bytes(cls.contents) - - for i in files: - with open(os.path.join(directory, f"file_{i}.txt"), "wb") as f: - f.write(cls.contents) - - os.symlink(f"file_{i}.txt", os.path.join(directory, f"link_{i}.txt")) - - @classmethod - def setup_class(cls): - cls.tmpdir = tempfile.TemporaryDirectory() - - # Create casync files - cls.manifest_fn = os.path.join(cls.tmpdir.name, 'orig.caibx') - cls.store_fn = os.path.join(cls.tmpdir.name, 'store') - - cls.directory_to_extract = tempfile.TemporaryDirectory() - cls.setup_cache(cls.directory_to_extract.name) - - cls.orig_fn = os.path.join(cls.tmpdir.name, 'orig.tar') - tar.create_tar_archive(cls.orig_fn, pathlib.Path(cls.directory_to_extract.name)) - - subprocess.check_output(["casync", "make", "--compression=xz", "--store", cls.store_fn, cls.manifest_fn, cls.orig_fn]) - - @classmethod - def teardown_class(cls): - cls.tmpdir.cleanup() - cls.directory_to_extract.cleanup() - - def setup_method(self): - self.cache_dir = tempfile.TemporaryDirectory() - self.working_dir = tempfile.TemporaryDirectory() - self.out_dir = tempfile.TemporaryDirectory() - - def teardown_method(self): - self.cache_dir.cleanup() - self.working_dir.cleanup() - self.out_dir.cleanup() - - def run_test(self): - target = casync.parse_caibx(self.manifest_fn) - - cache_filename = os.path.join(self.working_dir.name, "cache.tar") - tmp_filename = os.path.join(self.working_dir.name, "tmp.tar") - - sources = [('cache', casync.DirectoryTarChunkReader(self.cache_dir.name, cache_filename), casync.build_chunk_dict(target))] - sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))] - - stats = casync.extract_directory(target, sources, pathlib.Path(self.out_dir.name), tmp_filename) - - with open(os.path.join(self.out_dir.name, "file_0.txt"), "rb") as f: - assert f.read() == self.contents - - with open(os.path.join(self.out_dir.name, "link_0.txt"), "rb") as f: - assert f.read() == self.contents - assert os.readlink(os.path.join(self.out_dir.name, "link_0.txt")) == "file_0.txt" - - return stats - - def test_no_cache(self): - self.setup_cache(self.cache_dir.name, []) - stats = self.run_test() - assert stats['remote'] > 0 - assert stats['cache'] == 0 - - def test_full_cache(self): - self.setup_cache(self.cache_dir.name, range(self.NUM_FILES)) - stats = self.run_test() - assert stats['remote'] == 0 - assert stats['cache'] > 0 - - def test_one_file_cache(self): - self.setup_cache(self.cache_dir.name, range(1)) - stats = self.run_test() - assert stats['remote'] > 0 - assert stats['cache'] > 0 - assert stats['cache'] < stats['remote'] - - def test_one_file_incorrect_cache(self): - self.setup_cache(self.cache_dir.name, range(self.NUM_FILES)) - with open(os.path.join(self.cache_dir.name, "file_0.txt"), "wb") as f: - f.write(b"1234") - - stats = self.run_test() - assert stats['remote'] > 0 - assert stats['cache'] > 0 - assert stats['cache'] > stats['remote'] - - def test_one_file_missing_cache(self): - self.setup_cache(self.cache_dir.name, range(self.NUM_FILES)) - os.unlink(os.path.join(self.cache_dir.name, "file_12.txt")) - - stats = self.run_test() - assert stats['remote'] > 0 - assert stats['cache'] > 0 - assert stats['cache'] > stats['remote'] diff --git a/system/updated/tests/test_base.py b/system/updated/tests/test_base.py deleted file mode 100644 index db38bfa840..0000000000 --- a/system/updated/tests/test_base.py +++ /dev/null @@ -1,259 +0,0 @@ -import os -import pathlib -import shutil -import signal -import stat -import subprocess -import tempfile -import time -import pytest - -from openpilot.common.params import Params -from openpilot.system.manager.process import ManagerProcess -from openpilot.selfdrive.test.helpers import processes_context - - -def get_consistent_flag(path: str) -> bool: - consistent_file = pathlib.Path(os.path.join(path, ".overlay_consistent")) - return consistent_file.is_file() - - -def run(args, **kwargs): - return subprocess.check_output(args, **kwargs) - - -def update_release(directory, name, version, agnos_version, release_notes): - with open(directory / "RELEASES.md", "w") as f: - f.write(release_notes) - - (directory / "common").mkdir(exist_ok=True) - - with open(directory / "common" / "version.h", "w") as f: - f.write(f'#define COMMA_VERSION "{version}"') - - launch_env = directory / "launch_env.sh" - with open(launch_env, "w") as f: - f.write(f'export AGNOS_VERSION="{agnos_version}"') - - st = os.stat(launch_env) - os.chmod(launch_env, st.st_mode | stat.S_IEXEC) - - test_symlink = directory / "test_symlink" - if not os.path.exists(str(test_symlink)): - os.symlink("common/version.h", test_symlink) - - -def get_version(path: str) -> str: - with open(os.path.join(path, "common", "version.h")) as f: - return f.read().split('"')[1] - - -@pytest.mark.slow # TODO: can we test overlayfs in GHA? -class TestBaseUpdate: - @classmethod - def setup_class(cls): - if "Base" in cls.__name__: - pytest.skip() - - def setup_method(self): - self.tmpdir = tempfile.mkdtemp() - - run(["sudo", "mount", "-t", "tmpfs", "tmpfs", self.tmpdir]) # overlayfs doesn't work inside of docker unless this is a tmpfs - - self.mock_update_path = pathlib.Path(self.tmpdir) - - self.params = Params() - - self.basedir = self.mock_update_path / "openpilot" - self.basedir.mkdir() - - self.staging_root = self.mock_update_path / "safe_staging" - self.staging_root.mkdir() - - self.remote_dir = self.mock_update_path / "remote" - self.remote_dir.mkdir() - - os.environ["UPDATER_STAGING_ROOT"] = str(self.staging_root) - os.environ["UPDATER_LOCK_FILE"] = str(self.mock_update_path / "safe_staging_overlay.lock") - - self.MOCK_RELEASES = { - "release3": ("0.1.2", "1.2", "0.1.2 release notes"), - "master": ("0.1.3", "1.2", "0.1.3 release notes"), - } - - @pytest.fixture(autouse=True) - def mock_basedir(self, mocker): - mocker.patch("openpilot.common.basedir.BASEDIR", self.basedir) - - def set_target_branch(self, branch): - self.params.put("UpdaterTargetBranch", branch, block=True) - - def setup_basedir_release(self, release): - self.params = Params() - self.set_target_branch(release) - - def update_remote_release(self, release): - raise NotImplementedError("") - - def setup_remote_release(self, release): - raise NotImplementedError("") - - def additional_context(self): - raise NotImplementedError("") - - def teardown_method(self): - try: - run(["sudo", "umount", "-l", str(self.staging_root / "merged")]) - run(["sudo", "umount", "-l", self.tmpdir]) - shutil.rmtree(self.tmpdir) - except Exception: - print("cleanup failed...") - - def wait_for_condition(self, condition, timeout=12): - start = time.monotonic() - while True: - waited = time.monotonic() - start - if condition(): - print(f"waited {waited}s for condition ") - return waited - - if waited > timeout: - raise TimeoutError("timed out waiting for condition") - - time.sleep(1) - - def _test_finalized_update(self, branch, version, agnos_version, release_notes): - assert get_version(str(self.staging_root / "finalized")) == version - assert get_consistent_flag(str(self.staging_root / "finalized")) - assert os.access(str(self.staging_root / "finalized" / "launch_env.sh"), os.X_OK) - - with open(self.staging_root / "finalized" / "test_symlink") as f: - assert version in f.read() - -class ParamsBaseUpdateTest(TestBaseUpdate): - def _test_finalized_update(self, branch, version, agnos_version, release_notes): - assert self.params.get("UpdaterNewDescription").startswith(f"{version} / {branch}") - assert self.params.get("UpdaterNewReleaseNotes") == f"{release_notes}\n".encode() - super()._test_finalized_update(branch, version, agnos_version, release_notes) - - def send_check_for_updates_signal(self, updated: ManagerProcess): - updated.signal(signal.SIGUSR1.value) - - def send_download_signal(self, updated: ManagerProcess): - updated.signal(signal.SIGHUP.value) - - def _test_params(self, branch, fetch_available, update_available): - assert self.params.get("UpdaterTargetBranch") == branch - assert self.params.get_bool("UpdaterFetchAvailable") == fetch_available - assert self.params.get_bool("UpdateAvailable") == update_available - - def wait_for_idle(self): - self.wait_for_condition(lambda: self.params.get("UpdaterState") == "idle") - - def wait_for_failed(self): - self.wait_for_condition(lambda: self.params.get("UpdateFailedCount") is not None and \ - self.params.get("UpdateFailedCount") > 0) - - def wait_for_fetch_available(self): - self.wait_for_condition(lambda: self.params.get_bool("UpdaterFetchAvailable")) - - def wait_for_update_available(self): - self.wait_for_condition(lambda: self.params.get_bool("UpdateAvailable")) - - def test_no_update(self): - # Start on release3, ensure we don't fetch any updates - self.setup_remote_release("release3") - self.setup_basedir_release("release3") - - with self.additional_context(), processes_context(["updated"]) as [updated]: - self._test_params("release3", False, False) - self.wait_for_idle() - self._test_params("release3", False, False) - - self.send_check_for_updates_signal(updated) - - self.wait_for_idle() - - self._test_params("release3", False, False) - - def test_new_release(self): - # Start on release3, simulate a release3 commit, ensure we fetch that update properly - self.setup_remote_release("release3") - self.setup_basedir_release("release3") - - with self.additional_context(), processes_context(["updated"]) as [updated]: - self._test_params("release3", False, False) - self.wait_for_idle() - self._test_params("release3", False, False) - - self.MOCK_RELEASES["release3"] = ("0.1.3", "1.2", "0.1.3 release notes") - self.update_remote_release("release3") - - self.send_check_for_updates_signal(updated) - - self.wait_for_fetch_available() - - self._test_params("release3", True, False) - - self.send_download_signal(updated) - - self.wait_for_update_available() - - self._test_params("release3", False, True) - self._test_finalized_update("release3", *self.MOCK_RELEASES["release3"]) - - def test_switch_branches(self): - # Start on release3, request to switch to master manually, ensure we switched - self.setup_remote_release("release3") - self.setup_remote_release("master") - self.setup_basedir_release("release3") - - with self.additional_context(), processes_context(["updated"]) as [updated]: - self._test_params("release3", False, False) - self.wait_for_idle() - self._test_params("release3", False, False) - - self.set_target_branch("master") - self.send_check_for_updates_signal(updated) - - self.wait_for_fetch_available() - - self._test_params("master", True, False) - - self.send_download_signal(updated) - - self.wait_for_update_available() - - self._test_params("master", False, True) - self._test_finalized_update("master", *self.MOCK_RELEASES["master"]) - - def test_agnos_update(self, mocker): - # Start on release3, push an update with an agnos change - self.setup_remote_release("release3") - self.setup_basedir_release("release3") - - with self.additional_context(), processes_context(["updated"]) as [updated]: - mocker.patch("openpilot.system.hardware.AGNOS", "True") - mocker.patch("openpilot.system.hardware.tici.hardware.Tici.get_os_version", "1.2") - mocker.patch("openpilot.system.hardware.tici.agnos.get_target_slot_number") - mocker.patch("openpilot.system.hardware.tici.agnos.flash_agnos_update") - - self._test_params("release3", False, False) - self.wait_for_idle() - self._test_params("release3", False, False) - - self.MOCK_RELEASES["release3"] = ("0.1.3", "1.3", "0.1.3 release notes") - self.update_remote_release("release3") - - self.send_check_for_updates_signal(updated) - - self.wait_for_fetch_available() - - self._test_params("release3", True, False) - - self.send_download_signal(updated) - - self.wait_for_update_available() - - self._test_params("release3", False, True) - self._test_finalized_update("release3", *self.MOCK_RELEASES["release3"]) diff --git a/system/updated/tests/test_git.py b/system/updated/tests/test_git.py deleted file mode 100644 index 5a5a27000b..0000000000 --- a/system/updated/tests/test_git.py +++ /dev/null @@ -1,22 +0,0 @@ -import contextlib -from openpilot.system.updated.tests.test_base import ParamsBaseUpdateTest, run, update_release - - -class TestUpdateDGitStrategy(ParamsBaseUpdateTest): - def update_remote_release(self, release): - update_release(self.remote_dir, release, *self.MOCK_RELEASES[release]) - run(["git", "add", "."], cwd=self.remote_dir) - run(["git", "commit", "-m", f"openpilot release {release}"], cwd=self.remote_dir) - - def setup_remote_release(self, release): - run(["git", "init"], cwd=self.remote_dir) - run(["git", "checkout", "-b", release], cwd=self.remote_dir) - self.update_remote_release(release) - - def setup_basedir_release(self, release): - super().setup_basedir_release(release) - run(["git", "clone", "-b", release, self.remote_dir, self.basedir]) - - @contextlib.contextmanager - def additional_context(self): - yield diff --git a/system/updated/tests/test_updated.py b/system/updated/tests/test_updated.py deleted file mode 100644 index 18ee0e706a..0000000000 --- a/system/updated/tests/test_updated.py +++ /dev/null @@ -1,38 +0,0 @@ -import pytest - -from openpilot.common.params import Params -from openpilot.system.updated.updated import Updater - - -@pytest.mark.parametrize(("device_type", "branch", "expected"), [ - ("tizi", "release3", "release-tizi"), - ("tizi", "release3-staging", "release-tizi-staging"), - ("mici", "release3", "release-mici"), - ("mici", "release3-staging", "release-mici-staging"), -]) -def test_target_branch_migration_from_current_branch(mocker, device_type, branch, expected): - params = Params() - params.remove("UpdaterTargetBranch") - - mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type) - mocker.patch.object(Updater, "get_branch", return_value=branch) - - assert Updater().target_branch == expected - - -@pytest.mark.parametrize(("device_type", "branch", "expected"), [ - ("tizi", "release3", "release-tizi"), - ("tizi", "release3-staging", "release-tizi-staging"), - ("mici", "release3", "release-mici"), - ("mici", "release3-staging", "release-mici-staging"), -]) -def test_target_branch_migration_from_param(mocker, device_type, branch, expected): - params = Params() - params.put("UpdaterTargetBranch", branch, block=True) - - mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type) - - try: - assert Updater().target_branch == expected - finally: - params.remove("UpdaterTargetBranch") diff --git a/system/webrtc/webrtcd.py b/system/webrtc/webrtcd.py deleted file mode 100755 index aac020dd06..0000000000 --- a/system/webrtc/webrtcd.py +++ /dev/null @@ -1,419 +0,0 @@ -#!/usr/bin/env python3 - -from abc import abstractmethod -import os -import time -import argparse -import asyncio -import contextlib -import json -import uuid -import logging -from dataclasses import dataclass, field -from typing import Any, TYPE_CHECKING - -# aiortc and its dependencies have lots of internal warnings :( -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) -warnings.filterwarnings("ignore", category=RuntimeWarning) # TODO: remove this when google-crc32c publish a python3.12 wheel - -import capnp -from aiohttp import web -if TYPE_CHECKING: - from aiortc.rtcdatachannel import RTCDataChannel - -from openpilot.system.webrtc.schema import generate_field -from openpilot.common.params import Params -from cereal import messaging, log - - -class AsyncTaskRunner: - def __init__(self): - self.is_running = False - self.task = None - self.logger = logging.getLogger("webrtcd") - - def start(self): - assert self.task is None - self.task = asyncio.create_task(self.run()) - - async def stop(self): - if self.task is None: - return - task = self.task - self.task = None - if task.done(): - return - task.cancel() - with contextlib.suppress(asyncio.CancelledError): - await task - - @abstractmethod - async def run(self): - pass - - -class CerealOutgoingMessageProxy(AsyncTaskRunner): - def __init__(self, sm: messaging.SubMaster): - super().__init__() - self.sm = sm - self.channels: list[RTCDataChannel] = [] - - def add_channel(self, channel: 'RTCDataChannel'): - self.channels.append(channel) - - def to_json(self, msg_content: Any): - if isinstance(msg_content, capnp._DynamicStructReader): - msg_dict = msg_content.to_dict() - elif isinstance(msg_content, capnp._DynamicListReader): - msg_dict = [self.to_json(msg) for msg in msg_content] - elif isinstance(msg_content, bytes): - msg_dict = msg_content.decode() - else: - msg_dict = msg_content - - return msg_dict - - def update(self): - # this is blocking in async context... - self.sm.update(0) - for service, updated in self.sm.updated.items(): - if not updated: - continue - msg_dict = self.to_json(self.sm[service]) - mono_time, valid = self.sm.logMonoTime[service], self.sm.valid[service] - outgoing_msg = {"type": service, "logMonoTime": mono_time, "valid": valid, "data": msg_dict} - encoded_msg = json.dumps(outgoing_msg).encode() - for channel in self.channels: - channel.send(encoded_msg) - - async def run(self): - from aiortc.exceptions import InvalidStateError - - while True: - try: - self.update() - except InvalidStateError: - self.logger.warning("Cereal outgoing proxy invalid state (connection closed)") - break - except Exception: - self.logger.exception("Cereal outgoing proxy failure") - await asyncio.sleep(0.01) - - -class CerealIncomingMessageProxy: - def __init__(self, pm: messaging.PubMaster): - self.pm = pm - - def send(self, message: bytes): - msg_json = json.loads(message) - msg_type, msg_data = msg_json["type"], msg_json["data"] - size = None - if not isinstance(msg_data, dict): - size = len(msg_data) - - msg = messaging.new_message(msg_type, size=size) - setattr(msg, msg_type, msg_data) - self.pm.send(msg_type, msg) - - -class DynamicPubMaster(messaging.PubMaster): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.lock = asyncio.Lock() - - async def add_services_if_needed(self, services): - async with self.lock: - for service in services: - if service not in self.sock: - self.sock[service] = messaging.pub_sock(service) - - -class LivestreamBitrateController(AsyncTaskRunner): - bitrates = [500_000, 1_500_000, int(os.environ.get("STREAM_BITRATE", 5_000_000))] - label_to_bitrate = { "high": bitrates[2], "med": bitrates[1], "low": bitrates[0]} - sample_interval = 0.2 - high_level = 0.1 # drop immediately - med_level = 0.05 # drop after # of samples - low_level = 0 # raise after # of samples - down_samples = 5 # 1s - param_name = "LivestreamEncoderBitrate" - - def __init__(self, peer_connection: Any): - super().__init__() - self.pc = peer_connection - self.params = Params() - - self.level = 0 - self.prev_lost, self.prev_sent = None, None - self.counter = 0 - self.up_samples = 5 # 1s - self._auto = True - - async def run(self): - while True: - await asyncio.sleep(self.sample_interval) - if not self._auto: - continue - - loss_rate = await self._sample() - if loss_rate is None: - continue - if loss_rate >= self.med_level and self.level > 0: - self.counter += 1 - if self.counter >= self.down_samples or loss_rate >= self.high_level: - self.level -= 1 - self.up_samples *= 2 # exponential backoff before raising again - self.counter = 0 - self._publish(self.bitrates[self.level]) - elif loss_rate <= self.low_level and self.level < len(self.bitrates) - 1: - self.counter -= 1 - if -self.counter >= self.up_samples: - self.level += 1 - self.counter = 0 - self._publish(self.bitrates[self.level]) - - async def _sample(self) -> float | None: - report = await self.pc.getStats() - packets_lost = packets_sent = 0 - for s in report.values(): - if s.type == "remote-inbound-rtp": - packets_lost += s.packetsLost - elif s.type == "outbound-rtp": - packets_sent += s.packetsSent - - if self.prev_lost is None: - self.prev_lost, self.prev_sent = packets_lost, packets_sent - return None - lost_delta = max(0, packets_lost - self.prev_lost) - sent_delta = max(0, packets_sent - self.prev_sent) - self.prev_lost, self.prev_sent = packets_lost, packets_sent - return lost_delta / sent_delta if sent_delta else 0.0 - - def _publish(self, bitrate: float): - self.params.put(self.param_name, bitrate) - - def set_quality(self, quality): - if quality in self.label_to_bitrate: - self._publish(self.label_to_bitrate[quality]) - self._auto = False - elif quality == "auto": - self._auto = True - - -class StreamSession: - shared_pub_master = DynamicPubMaster([]) - - def __init__(self, sdp: str, init_camera: str, incoming_services: list[str], outgoing_services: list[str], debug_mode: bool = False): - from aiortc.mediastreams import VideoStreamTrack - from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack - from teleoprtc import WebRTCAnswerBuilder - - builder = WebRTCAnswerBuilder(sdp) - - self.video_track = LiveStreamVideoStreamTrack(init_camera) if not debug_mode else VideoStreamTrack() - builder.add_video_stream(init_camera, self.video_track) - - self.stream = builder.stream() - self.identifier = str(uuid.uuid4()) - - self.incoming_bridge: CerealIncomingMessageProxy | None = None - self.incoming_bridge_services = incoming_services - self.outgoing_bridge: CerealOutgoingMessageProxy | None = None - self.bitrate_controller: LivestreamBitrateController | None = None - if len(incoming_services) > 0: - self.incoming_bridge = CerealIncomingMessageProxy(self.shared_pub_master) - if len(outgoing_services) > 0: - self.outgoing_bridge = CerealOutgoingMessageProxy(messaging.SubMaster(outgoing_services)) - self.bitrate_controller = LivestreamBitrateController(self.stream.peer_connection) - - self.run_task: asyncio.Task | None = None - self._cleanup_lock = asyncio.Lock() - self._cleanup_done = False - self.logger = logging.getLogger("webrtcd") - self.logger.info( - "New stream session (%s), init camera %s, incoming services %s, outgoing services %s", - self.identifier, init_camera, incoming_services, outgoing_services, - ) - - def start(self): - self.run_task = asyncio.create_task(self.run()) - - async def stop(self): - if self.run_task is not None and not self.run_task.done() and self.run_task is not asyncio.current_task(): - self.run_task.cancel() - with contextlib.suppress(asyncio.CancelledError): - await self.run_task - self.run_task = None - await self.post_run_cleanup() - - async def get_answer(self): - return await self.stream.start() - - def message_handler(self, message: bytes): - assert self.incoming_bridge is not None - try: - payload = json.loads(message) if isinstance(message, (bytes, str)) else None - if isinstance(payload, dict): - msg_type = payload.get("type") - - match msg_type: - case "livestreamCameraSwitch": - self.video_track.switch_camera(payload["data"]["camera"]) - case "livestreamSettings": - self.bitrate_controller.set_quality(payload["data"]["quality"]) - case "clockSync": - pong = json.dumps({"type": "clockSync", "data": { - "action": "pong", "browserSendTime": payload["data"]["browserSendTime"], "deviceTime": time.time() * 1000, # noqa: TID251 - }}) - self.stream.get_messaging_channel().send(pong) - case "enableTimingSei": - if hasattr(self.video_track, 'timing_sei_enabled'): - self.video_track.timing_sei_enabled = bool(payload["data"]["enabled"]) - case _: - if payload.get("type") not in self.incoming_bridge_services: - return - self.incoming_bridge.send(message) - except Exception: - self.logger.exception("Cereal incoming proxy failure") - - async def run(self): - try: - await self.stream.wait_for_connection() - if self.stream.has_messaging_channel(): - if self.incoming_bridge is not None: - await self.shared_pub_master.add_services_if_needed(self.incoming_bridge_services) - self.stream.set_message_handler(self.message_handler) - if self.outgoing_bridge is not None: - channel = self.stream.get_messaging_channel() - self.outgoing_bridge.add_channel(channel) - self.outgoing_bridge.start() - self.bitrate_controller.start() - - self.logger.info("Stream session (%s) connected", self.identifier) - - await self.stream.wait_for_disconnection() - - self.logger.info("Stream session (%s) ended", self.identifier) - except Exception: - self.logger.exception("Stream session failure") - finally: - await self.post_run_cleanup() - - async def post_run_cleanup(self): - async with self._cleanup_lock: - if self._cleanup_done: - return - self._cleanup_done = True - if self.bitrate_controller is not None: - await self.bitrate_controller.stop() - if self.outgoing_bridge is not None: - await self.outgoing_bridge.stop() - await self.stream.stop() - - -@dataclass -class StreamRequestBody: - sdp: str - initCamera: str - bridge_services_in: list[str] = field(default_factory=list) - bridge_services_out: list[str] = field(default_factory=list) - - -async def get_stream(request: 'web.Request'): - stream_dict, debug_mode = request.app['streams'], request.app['debug'] - raw_body = await request.json() - body = StreamRequestBody(**raw_body) - - async with request.app['stream_lock']: - # Fully disconnect any other active stream before starting the replacement. - for sid, s in list(stream_dict.items()): - if s.run_task and not s.run_task.done(): - try: - ch = s.stream.get_messaging_channel() - ch.send(json.dumps({"type": "connectionReplaced", "data": "Another device has connected, closing this session."})) - except Exception: - pass - await s.stop() - del stream_dict[sid] - - session = StreamSession(body.sdp, body.initCamera, body.bridge_services_in, body.bridge_services_out, debug_mode) - try: - answer = await session.get_answer() - except ValueError as e: - await session.stop() - raise web.HTTPBadRequest( - text=json.dumps({"error": "invalid_sdp", "message": str(e)}), - content_type="application/json", - ) from e - except Exception: - await session.stop() - raise - session.start() - - stream_dict[session.identifier] = session - - return web.json_response({"sdp": answer.sdp, "type": answer.type}) - - -async def get_schema(request: 'web.Request'): - services = request.query["services"].split(",") - services = [s for s in services if s] - assert all(s in log.Event.schema.fields and not s.endswith("DEPRECATED") for s in services), "Invalid service name" - schema_dict = {s: generate_field(log.Event.schema.fields[s]) for s in services} - return web.json_response(schema_dict) - - -async def post_notify(request: 'web.Request'): - try: - payload = await request.json() - except Exception as e: - raise web.HTTPBadRequest(text="Invalid JSON") from e - - for session in list(request.app.get('streams', {}).values()): - try: - ch = session.stream.get_messaging_channel() - ch.send(json.dumps(payload)) - except Exception: - continue - - return web.Response(status=200, text="OK") - - -async def on_shutdown(app: 'web.Application'): - for session in app['streams'].values(): - await session.stop() - del app['streams'] - - -def webrtcd_thread(host: str, port: int, debug: bool): - logging.basicConfig(level=logging.CRITICAL, handlers=[logging.StreamHandler()]) - logging_level = logging.DEBUG if debug else logging.INFO - logging.getLogger("WebRTCStream").setLevel(logging_level) - logging.getLogger("webrtcd").setLevel(logging_level) - - app = web.Application() - - app['streams'] = dict() - app['stream_lock'] = asyncio.Lock() - app['debug'] = debug - app.on_shutdown.append(on_shutdown) - app.router.add_post("/stream", get_stream) - app.router.add_post("/notify", post_notify) - app.router.add_get("/schema", get_schema) - - web.run_app(app, host=host, port=port) - - -def main(): - parser = argparse.ArgumentParser(description="WebRTC daemon") - parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to listen on") - parser.add_argument("--port", type=int, default=5001, help="Port to listen on") - parser.add_argument("--debug", action="store_true", help="Enable debug mode") - args = parser.parse_args() - - webrtcd_thread(args.host, args.port, args.debug) - - -if __name__=="__main__": - main() diff --git a/teleoprtc b/teleoprtc deleted file mode 120000 index 3d3dbc8dea..0000000000 --- a/teleoprtc +++ /dev/null @@ -1 +0,0 @@ -teleoprtc_repo/teleoprtc \ No newline at end of file diff --git a/teleoprtc_repo b/teleoprtc_repo index 22df577821..c0f813f1c4 160000 --- a/teleoprtc_repo +++ b/teleoprtc_repo @@ -1 +1 @@ -Subproject commit 22df577821862e32a011fb0cf42577599f3a79c4 +Subproject commit c0f813f1c4f7e2d29bc0ed6a4d2a7b3268511b0f diff --git a/tinygrad b/tinygrad deleted file mode 120000 index cb003823c6..0000000000 --- a/tinygrad +++ /dev/null @@ -1 +0,0 @@ -tinygrad_repo/tinygrad \ No newline at end of file diff --git a/tools/CTF.md b/tools/CTF.md index 32891cd389..2b877a0e25 100644 --- a/tools/CTF.md +++ b/tools/CTF.md @@ -5,16 +5,16 @@ Welcome to the first part of the comma CTF! * there's 2 flags in each segment, with roughly increasing difficulty * everything you'll need to find the flags is in the openpilot repo * grep is also your friend - * first, [setup](https://github.com/commaai/openpilot/tree/master/tools#setup-your-pc) your PC - * read the docs & checkout out the tools in tools/ and selfdrive/debug/ + * first, [setup](https://github.com/commaai/openpilot/tree/master/openpilot/tools#setup-your-pc) your PC + * read the docs & checkout out the tools in openpilot/tools/ * tip: once you get the replay and UI up, start by familiarizing yourself with seeking in replay getting started ```bash # start the route replay -cd tools/replay +cd openpilot/tools/replay ./replay '0c7f0c7f0c7f0c7f|2021-10-13--13-00-00' --dcam --ecam # start the UI in another terminal -selfdrive/ui/ui +openpilot/selfdrive/ui/ui ``` diff --git a/tools/bodyteleop/static/index.html b/tools/bodyteleop/static/index.html deleted file mode 100644 index 48672dbbf0..0000000000 --- a/tools/bodyteleop/static/index.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - commabody - - - - - - - - - - -
-

comma body

- -
-
-
-
-
W
-
-
-
A
-
S
-
D
-
-
-
-
-
-
-
-
-
- - -
-

ping time

-
-
-
- - -
-

battery

-
-
-
-
-
-
-
-
-
-
-
- - - diff --git a/tools/bodyteleop/static/js/controls.js b/tools/bodyteleop/static/js/controls.js deleted file mode 100644 index 3a11f78b9e..0000000000 --- a/tools/bodyteleop/static/js/controls.js +++ /dev/null @@ -1,20 +0,0 @@ -const keyVals = {w: 0, a: 0, s: 0, d: 0} - -export function getXY() { - let x = -keyVals.w + keyVals.s - let y = -keyVals.d + keyVals.a - return {x, y} -} - -export const handleKeyX = (key, setValue) => { - if (['w', 'a', 's', 'd'].includes(key)){ - keyVals[key] = setValue; - let color = "#333"; - if (setValue === 1){ - color = "#e74c3c"; - } - $("#key-"+key).css('background', color); - const {x, y} = getXY(); - $("#pos-vals").text(x+","+y); - } -}; diff --git a/tools/bodyteleop/static/js/jsmain.js b/tools/bodyteleop/static/js/jsmain.js deleted file mode 100644 index 0db1dcd9b3..0000000000 --- a/tools/bodyteleop/static/js/jsmain.js +++ /dev/null @@ -1,21 +0,0 @@ -import { handleKeyX } from "./controls.js"; -import { start, stop, lastChannelMessageTime } from "./webrtc.js"; - -export var pc = null; -export var dc = null; - -document.addEventListener('keydown', (e)=>(handleKeyX(e.key.toLowerCase(), 1))); -document.addEventListener('keyup', (e)=>(handleKeyX(e.key.toLowerCase(), 0))); -$(".keys").bind("mousedown touchstart", (e)=>handleKeyX($(e.target).attr('id').replace('key-', ''), 1)); -$(".keys").bind("mouseup touchend", (e)=>handleKeyX($(e.target).attr('id').replace('key-', ''), 0)); -setInterval( () => { - const dt = new Date().getTime(); - if ((dt - lastChannelMessageTime) > 1000) { - $(".pre-blob").removeClass('blob'); - $("#battery").text("-"); - $("#ping-time").text('-'); - $("video")[0].load(); - } -}, 5000); - -start(pc, dc); diff --git a/tools/bodyteleop/static/js/plots.js b/tools/bodyteleop/static/js/plots.js deleted file mode 100644 index 5327bf71be..0000000000 --- a/tools/bodyteleop/static/js/plots.js +++ /dev/null @@ -1,53 +0,0 @@ -export const pingPoints = []; -export const batteryPoints = []; - -function getChartConfig(pts, color, title, ymax=100) { - return { - type: 'line', - data: { - datasets: [{ - label: title, - data: pts, - borderWidth: 1, - borderColor: color, - backgroundColor: color, - fill: 'origin' - }] - }, - options: { - scales: { - x: { - type: 'time', - time: { - unit: 'minute', - displayFormats: { - second: 'h:mm a' - } - }, - grid: { - color: '#222', // Grid lines color - }, - ticks: { - source: 'data', - fontColor: 'rgba(255, 255, 255, 1.0)', // Y-axis label color - } - }, - y: { - beginAtZero: true, - max: ymax, - grid: { - color: 'rgba(255, 255, 255, 0.1)', // Grid lines color - }, - ticks: { - fontColor: 'rgba(255, 255, 255, 0.7)', // Y-axis label color - } - } - } - } - } -} - -const ctxPing = document.getElementById('chart-ping'); -const ctxBattery = document.getElementById('chart-battery'); -export const chartPing = new Chart(ctxPing, getChartConfig(pingPoints, 'rgba(192, 57, 43, 0.7)', 'Controls Ping Time (ms)', 250)); -export const chartBattery = new Chart(ctxBattery, getChartConfig(batteryPoints, 'rgba(41, 128, 185, 0.7)', 'Battery %', 100)); diff --git a/tools/bodyteleop/static/js/webrtc.js b/tools/bodyteleop/static/js/webrtc.js deleted file mode 100644 index 28bea238e6..0000000000 --- a/tools/bodyteleop/static/js/webrtc.js +++ /dev/null @@ -1,175 +0,0 @@ -import { getXY } from "./controls.js"; -import { pingPoints, batteryPoints, chartPing, chartBattery } from "./plots.js"; - -export let controlCommandInterval = null; -export let latencyInterval = null; -export let lastChannelMessageTime = null; - - -export function offerRtcRequest(sdp, type) { - return fetch('/offer', { - body: JSON.stringify({sdp: sdp, type: type}), - headers: {'Content-Type': 'application/json'}, - method: 'POST' - }); -} - - -export function pingHeadRequest() { - return fetch('/', { - method: 'HEAD' - }); -} - - -export function createPeerConnection(pc) { - var config = { - sdpSemantics: 'unified-plan' - }; - - pc = new RTCPeerConnection(config); - - // connect video - pc.addEventListener('track', function(evt) { - console.log("Adding Tracks!") - if (evt.track.kind == 'video') - document.getElementById('video').srcObject = evt.streams[0]; - }); - return pc; -} - - -export function negotiate(pc) { - return pc.createOffer({offerToReceiveVideo:true}).then(function(offer) { - return pc.setLocalDescription(offer); - }).then(function() { - return new Promise(function(resolve) { - if (pc.iceGatheringState === 'complete') { - resolve(); - } - else { - function checkState() { - if (pc.iceGatheringState === 'complete') { - pc.removeEventListener('icegatheringstatechange', checkState); - resolve(); - } - } - pc.addEventListener('icegatheringstatechange', checkState); - } - }); - }).then(function() { - var offer = pc.localDescription; - return offerRtcRequest(offer.sdp, offer.type); - }).then(function(response) { - console.log(response); - return response.json(); - }).then(function(answer) { - return pc.setRemoteDescription(answer); - }).catch(function(e) { - alert(e); - }); -} - - -function isMobile() { - let check = false; - (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); - return check; -}; - - -export const constraints = { - video: isMobile() -}; - - -export function start(pc, dc) { - pc = createPeerConnection(pc); - - // add a local video track on mobile - (constraints.video ? navigator.mediaDevices.getUserMedia(constraints) : Promise.resolve(null)) - .then(function(stream) { - if (stream) { - stream.getTracks().forEach(function(track) { - pc.addTrack(track, stream); - }); - } - - return negotiate(pc); - }) - .catch(function(err) { - alert('Could not acquire media: ' + err); - }); - - var parameters = {"ordered": true}; - dc = pc.createDataChannel('data', parameters); - dc.onclose = function() { - clearInterval(controlCommandInterval); - clearInterval(latencyInterval); - }; - - function sendJoystickOverDataChannel() { - const {x, y} = getXY(); - var message = JSON.stringify({type: "testJoystick", data: {axes: [x, y], buttons: [false]}}) - dc.send(message); - } - function checkLatency() { - const initialTime = new Date().getTime(); - pingHeadRequest().then(function() { - const currentTime = new Date().getTime(); - if (Math.abs(currentTime - lastChannelMessageTime) < 1000) { - const pingtime = currentTime - initialTime; - pingPoints.push({'x': currentTime, 'y': pingtime}); - if (pingPoints.length > 1000) { - pingPoints.shift(); - } - chartPing.update(); - $("#ping-time").text((pingtime) + "ms"); - } - }) - } - dc.onopen = function() { - controlCommandInterval = setInterval(sendJoystickOverDataChannel, 50); - latencyInterval = setInterval(checkLatency, 1000); - sendJoystickOverDataChannel(); - }; - - const textDecoder = new TextDecoder(); - var carStaterIndex = 0; - dc.onmessage = function(evt) { - const text = textDecoder.decode(evt.data); - const msg = JSON.parse(text); - if (carStaterIndex % 100 == 0 && msg.type === 'carState') { - const batteryLevel = Math.round(msg.data.fuelGauge * 100); - $("#battery").text(batteryLevel + "%"); - batteryPoints.push({'x': new Date().getTime(), 'y': batteryLevel}); - if (batteryPoints.length > 1000) { - batteryPoints.shift(); - } - chartBattery.update(); - } - carStaterIndex += 1; - lastChannelMessageTime = new Date().getTime(); - $(".pre-blob").addClass('blob'); - }; -} - - -export function stop(pc, dc) { - if (dc) { - dc.close(); - } - if (pc.getTransceivers) { - pc.getTransceivers().forEach(function(transceiver) { - if (transceiver.stop) { - transceiver.stop(); - } - }); - } - pc.getSenders().forEach(function(sender) { - sender.track.stop(); - }); - setTimeout(function() { - pc.close(); - }, 500); -} diff --git a/tools/bodyteleop/static/main.css b/tools/bodyteleop/static/main.css deleted file mode 100644 index 79fe8052ff..0000000000 --- a/tools/bodyteleop/static/main.css +++ /dev/null @@ -1,178 +0,0 @@ -body { - background: #333 !important; - color: #fff !important; - display: flex; - justify-content: center; - align-items: start; -} - -p { - margin: 0px !important; -} - -i { - font-style: normal; -} - -.small { - font-size: 1em !important -} - -.jumbo { - font-size: 8rem; -} - - -@media (max-width: 600px) { - .small { - font-size: 0.5em !important - } - .jumbo { - display: none; - } - -} - -#main { - display: flex; - flex-direction: column; - align-content: center; - justify-content: center; - align-items: center; - font-size: 30px; - width: 100%; - max-width: 1200px; -} - -video { - width: 95%; -} - -.pre-blob { - display: flex; - background: #333; - border-radius: 50%; - margin: 10px; - height: 45px; - width: 45px; - justify-content: center; - align-items: center; - font-size: 1rem; -} - -.blob { - background: rgba(231, 76, 60,1.0); - box-shadow: 0 0 0 0 rgba(231, 76, 60,1.0); - animation: pulse 2s infinite; -} - -@keyframes pulse { - 0% { - box-shadow: 0 0 0 0px rgba(192, 57, 43, 1); - } - 100% { - box-shadow: 0 0 0 20px rgba(192, 57, 43, 0); - } -} - - -.icon-sup-panel { - display: flex; - flex-direction: row; - justify-content: space-around; - align-items: center; - background: #222; - border-radius: 10px; - padding: 5px; - margin: 5px 0px 5px 0px; -} - -.icon-sub-panel { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; -} - -#icon-panel { - display: flex; - width: 100%; - justify-content: space-between; - margin-top: 5px; -} - -.icon-sub-sub-panel { - display: flex; - flex-direction: row; -} - -.keys, #key-val { - background: #333; - padding: 2rem; - margin: 5px; - color: #fff; - display: flex; - justify-content: center; - align-items: center; - border-radius: 10px; - cursor: pointer; -} - -#key-val { - pointer-events: none; - background: #fff; - color: #333; - line-height: 1; - font-size: 20px; - flex-direction: column; -} - -.wasd-row { - display: flex; - flex-direction: row; - justify-content: center; - align-items: stretch; -} - -#wasd { - margin: 5px 0px 5px 0px; - background: #222; - border-radius: 10px; - width: 100%; - padding: 20px; - display: flex; - flex-direction: row; - justify-content: space-around; - align-items: stretch; - - user-select: none; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - touch-action: manipulation; -} - -.panel { - display: flex; - justify-content: center; - margin: 5px 0px 5px 0px !important; - background: #222; - border-radius: 10px; - width: 100%; - padding: 10px; -} - -#ping-time, #battery { - font-size: 25px; -} - -#stop { - display: none; -} - -.details { - display: flex; - padding: 0px 10px 0px 10px; -} diff --git a/tools/bodyteleop/static/poster.png b/tools/bodyteleop/static/poster.png deleted file mode 100644 index 2f2b02dd8a..0000000000 --- a/tools/bodyteleop/static/poster.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8740da2be7faac198b5e10780c646166056a76ebbe3d64499e0cdc49280c8a4f -size 8297 diff --git a/tools/bodyteleop/web.py b/tools/bodyteleop/web.py deleted file mode 100644 index d357561b22..0000000000 --- a/tools/bodyteleop/web.py +++ /dev/null @@ -1,86 +0,0 @@ -import dataclasses -import json -import logging -import os -import ssl -import subprocess - -from aiohttp import web -from aiohttp import ClientSession - -from openpilot.common.basedir import BASEDIR -from openpilot.system.webrtc.webrtcd import StreamRequestBody -from openpilot.common.params import Params - -logger = logging.getLogger("bodyteleop") -logging.basicConfig(level=logging.INFO) - -TELEOPDIR = f"{BASEDIR}/tools/bodyteleop" -WEBRTCD_HOST, WEBRTCD_PORT = "localhost", 5001 - - -## SSL -def create_ssl_cert(cert_path: str, key_path: str): - try: - proc = subprocess.run(f'openssl req -x509 -newkey rsa:4096 -nodes -out {cert_path} -keyout {key_path} \ - -days 365 -subj "/C=US/ST=California/O=commaai/OU=comma body"', - capture_output=True, shell=True) - proc.check_returncode() - except subprocess.CalledProcessError as ex: - raise ValueError(f"Error creating SSL certificate:\n[stdout]\n{proc.stdout.decode()}\n[stderr]\n{proc.stderr.decode()}") from ex - - -def create_ssl_context(): - cert_path = os.path.join(TELEOPDIR, "cert.pem") - key_path = os.path.join(TELEOPDIR, "key.pem") - if not os.path.exists(cert_path) or not os.path.exists(key_path): - logger.info("Creating certificate...") - create_ssl_cert(cert_path, key_path) - else: - logger.info("Certificate exists!") - ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER) - ssl_context.load_cert_chain(cert_path, key_path) - - return ssl_context - -## ENDPOINTS -async def index(request: 'web.Request'): - with open(os.path.join(TELEOPDIR, "static", "index.html")) as f: - content = f.read() - return web.Response(content_type="text/html", text=content) - - -async def ping(request: 'web.Request'): - return web.Response(text="pong") - - -async def offer(request: 'web.Request'): - params = await request.json() - body = StreamRequestBody(params["sdp"], "driver", ["testJoystick"], ["carState"]) - body_json = json.dumps(dataclasses.asdict(body)) - - logger.info("Sending offer to webrtcd...") - webrtcd_url = f"http://{WEBRTCD_HOST}:{WEBRTCD_PORT}/stream" - async with ClientSession() as session, session.post(webrtcd_url, data=body_json) as resp: - assert resp.status == 200 - answer = await resp.json() - return web.json_response(answer) - - -def main(): - # Enable joystick debug mode - Params().put_bool("JoystickDebugMode", True, block=True) - - # App needs to be HTTPS for WebRTC to work on the browser - ssl_context = create_ssl_context() - - app = web.Application() - app.router.add_get("/", index) - app.router.add_get("/ping", ping, allow_head=True) - app.router.add_post("/offer", offer) - app.router.add_static('/static', os.path.join(TELEOPDIR, 'static')) - web.run_app(app, access_log=None, host="0.0.0.0", port=5000, ssl_context=ssl_context) - - -if __name__ == "__main__": - main() diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc deleted file mode 100644 index db26b4067a..0000000000 --- a/tools/cabana/cabana.cc +++ /dev/null @@ -1,88 +0,0 @@ -#include -#include - -#include "tools/cabana/mainwin.h" -#include "tools/cabana/streams/devicestream.h" -#include "tools/cabana/streams/pandastream.h" -#include "tools/cabana/streams/replaystream.h" -#ifdef __linux__ -#include "tools/cabana/streams/socketcanstream.h" -#endif - -int main(int argc, char *argv[]) { - QCoreApplication::setApplicationName("Cabana"); - QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); - initApp(argc, argv, false); - QApplication app(argc, argv); - app.setApplicationDisplayName("Cabana"); - app.setWindowIcon(QIcon(":cabana-icon.png")); - - UnixSignalHandler signalHandler; - utils::setTheme(settings.theme); - - QCommandLineParser cmd_parser; - cmd_parser.addHelpOption(); - cmd_parser.addPositionalArgument("route", "the drive to replay. find your drives at connect.comma.ai"); - cmd_parser.addOption({"demo", "use a demo route instead of providing your own"}); - cmd_parser.addOption({"auto", "Auto load the route from the best available source (no video): internal, openpilotci, comma_api, car_segments, testing_closet"}); - cmd_parser.addOption({"qcam", "load qcamera"}); - cmd_parser.addOption({"ecam", "load wide road camera"}); - cmd_parser.addOption({"dcam", "load driver camera"}); - cmd_parser.addOption({"msgq", "read can messages from the msgq"}); - cmd_parser.addOption({"panda", "read can messages from panda"}); - cmd_parser.addOption({"panda-serial", "read can messages from panda with given serial", "panda-serial"}); -#ifdef __linux__ - if (SocketCanStream::available()) { - cmd_parser.addOption({"socketcan", "read can messages from given SocketCAN device", "socketcan"}); - } -#endif - cmd_parser.addOption({"zmq", "read can messages from zmq at the specified ip-address", "ip-address"}); - cmd_parser.addOption({"data_dir", "local directory with routes", "data_dir"}); - cmd_parser.addOption({"no-vipc", "do not output video"}); - cmd_parser.addOption({"dbc", "dbc file to open", "dbc"}); - cmd_parser.process(app); - - AbstractStream *stream = nullptr; - - if (cmd_parser.isSet("msgq")) { - stream = new DeviceStream(&app); - } else if (cmd_parser.isSet("zmq")) { - stream = new DeviceStream(&app, cmd_parser.value("zmq")); - } else if (cmd_parser.isSet("panda") || cmd_parser.isSet("panda-serial")) { - try { - stream = new PandaStream(&app, {.serial = cmd_parser.value("panda-serial").toStdString()}); - } catch (std::exception &e) { - qWarning() << e.what(); - return 0; - } -#ifdef __linux__ - } else if (SocketCanStream::available() && cmd_parser.isSet("socketcan")) { - stream = new SocketCanStream(&app, {.device = cmd_parser.value("socketcan").toStdString()}); -#endif - } else { - uint32_t replay_flags = REPLAY_FLAG_NONE; - if (cmd_parser.isSet("ecam")) replay_flags |= REPLAY_FLAG_ECAM; - if (cmd_parser.isSet("qcam")) replay_flags |= REPLAY_FLAG_QCAMERA; - if (cmd_parser.isSet("dcam")) replay_flags |= REPLAY_FLAG_DCAM; - if (cmd_parser.isSet("no-vipc")) replay_flags |= REPLAY_FLAG_NO_VIPC; - - const QStringList args = cmd_parser.positionalArguments(); - QString route; - if (args.size() > 0) { - route = args.first(); - } else if (cmd_parser.isSet("demo")) { - route = DEMO_ROUTE; - } - if (!route.isEmpty()) { - auto replay_stream = std::make_unique(&app); - bool auto_source = cmd_parser.isSet("auto"); - if (!replay_stream->loadRoute(route.toStdString(), cmd_parser.value("data_dir").toStdString(), replay_flags, auto_source)) { - return 0; - } - stream = replay_stream.release(); - } - } - - MainWindow w(stream, cmd_parser.value("dbc")); - return app.exec(); -} diff --git a/tools/cabana/cameraview.cc b/tools/cabana/cameraview.cc deleted file mode 100644 index 13c838efd8..0000000000 --- a/tools/cabana/cameraview.cc +++ /dev/null @@ -1,261 +0,0 @@ -#include "tools/cabana/cameraview.h" - -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include - -namespace { - -const char frame_vertex_shader[] = -#ifdef __APPLE__ - "#version 330 core\n" -#else - "#version 300 es\n" -#endif - "layout(location = 0) in vec4 aPosition;\n" - "layout(location = 1) in vec2 aTexCoord;\n" - "uniform mat4 uTransform;\n" - "out vec2 vTexCoord;\n" - "void main() {\n" - " gl_Position = uTransform * aPosition;\n" - " vTexCoord = aTexCoord;\n" - "}\n"; - -const char frame_fragment_shader[] = -#ifdef __APPLE__ - "#version 330 core\n" -#else - "#version 300 es\n" - "precision mediump float;\n" -#endif - "uniform sampler2D uTextureY;\n" - "uniform sampler2D uTextureUV;\n" - "in vec2 vTexCoord;\n" - "out vec4 colorOut;\n" - "void main() {\n" - " float y = texture(uTextureY, vTexCoord).r;\n" - " vec2 uv = texture(uTextureUV, vTexCoord).rg - 0.5;\n" - " float r = y + 1.402 * uv.y;\n" - " float g = y - 0.344 * uv.x - 0.714 * uv.y;\n" - " float b = y + 1.772 * uv.x;\n" - " colorOut = vec4(r, g, b, 1.0);\n" - "}\n"; - -} // namespace - -CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, QWidget* parent) : - stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QOpenGLWidget(parent) { - setAttribute(Qt::WA_OpaquePaintEvent); - qRegisterMetaType>("availableStreams"); - QObject::connect(this, &CameraWidget::vipcThreadConnected, this, &CameraWidget::vipcConnected, Qt::BlockingQueuedConnection); - QObject::connect(this, &CameraWidget::vipcThreadFrameReceived, this, &CameraWidget::vipcFrameReceived, Qt::QueuedConnection); - QObject::connect(this, &CameraWidget::vipcAvailableStreamsUpdated, this, &CameraWidget::availableStreamsUpdated, Qt::QueuedConnection); - QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit, this, &CameraWidget::stopVipcThread); -} - -CameraWidget::~CameraWidget() { - makeCurrent(); - stopVipcThread(); - if (isValid()) { - glDeleteVertexArrays(1, &frame_vao); - glDeleteBuffers(1, &frame_vbo); - glDeleteBuffers(1, &frame_ibo); - glDeleteTextures(2, textures); - shader_program_.reset(); - } - doneCurrent(); -} - -void CameraWidget::initializeGL() { - initializeOpenGLFunctions(); - - shader_program_ = std::make_unique(context()); - shader_program_->addShaderFromSourceCode(QOpenGLShader::Vertex, frame_vertex_shader); - shader_program_->addShaderFromSourceCode(QOpenGLShader::Fragment, frame_fragment_shader); - shader_program_->link(); - - GLint frame_pos_loc = shader_program_->attributeLocation("aPosition"); - GLint frame_texcoord_loc = shader_program_->attributeLocation("aTexCoord"); - - auto [x1, x2, y1, y2] = requested_stream_type == VISION_STREAM_DRIVER ? std::tuple(0.f, 1.f, 1.f, 0.f) : std::tuple(1.f, 0.f, 1.f, 0.f); - const uint8_t frame_indicies[] = {0, 1, 2, 0, 2, 3}; - const float frame_coords[4][4] = { - {-1.0, -1.0, x2, y1}, // bl - {-1.0, 1.0, x2, y2}, // tl - { 1.0, 1.0, x1, y2}, // tr - { 1.0, -1.0, x1, y1}, // br - }; - - glGenVertexArrays(1, &frame_vao); - glBindVertexArray(frame_vao); - glGenBuffers(1, &frame_vbo); - glBindBuffer(GL_ARRAY_BUFFER, frame_vbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(frame_coords), frame_coords, GL_STATIC_DRAW); - glEnableVertexAttribArray(frame_pos_loc); - glVertexAttribPointer(frame_pos_loc, 2, GL_FLOAT, GL_FALSE, - sizeof(frame_coords[0]), (const void *)0); - glEnableVertexAttribArray(frame_texcoord_loc); - glVertexAttribPointer(frame_texcoord_loc, 2, GL_FLOAT, GL_FALSE, - sizeof(frame_coords[0]), (const void *)(sizeof(float) * 2)); - glGenBuffers(1, &frame_ibo); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, frame_ibo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(frame_indicies), frame_indicies, GL_STATIC_DRAW); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindVertexArray(0); - - glGenTextures(2, textures); - - shader_program_->bind(); - shader_program_->setUniformValue("uTextureY", 0); - shader_program_->setUniformValue("uTextureUV", 1); - shader_program_->release(); -} - -void CameraWidget::showEvent(QShowEvent *event) { - if (!vipc_thread) { - clearFrames(); - vipc_thread = new QThread(); - connect(vipc_thread, &QThread::started, [=]() { vipcThread(); }); - connect(vipc_thread, &QThread::finished, vipc_thread, &QObject::deleteLater); - vipc_thread->start(); - } -} - -void CameraWidget::stopVipcThread() { - makeCurrent(); - if (vipc_thread) { - vipc_thread->requestInterruption(); - vipc_thread->quit(); - vipc_thread->wait(); - vipc_thread = nullptr; - } -} - -void CameraWidget::availableStreamsUpdated(std::set streams) { - available_streams = streams; -} - -void CameraWidget::paintGL() { - glClearColor(bg.redF(), bg.greenF(), bg.blueF(), bg.alphaF()); - glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT); - - std::lock_guard lk(frame_lock); - if (!current_frame_) return; - - // Scale for aspect ratio - float widget_ratio = (float)width() / height(); - float frame_ratio = (float)stream_width / stream_height; - float scale_x = std::min(frame_ratio / widget_ratio, 1.0f); - float scale_y = std::min(widget_ratio / frame_ratio, 1.0f); - - glViewport(0, 0, width() * devicePixelRatio(), height() * devicePixelRatio()); - - shader_program_->bind(); - QMatrix4x4 transform; - transform.scale(scale_x, scale_y, 1.0f); - shader_program_->setUniformValue("uTransform", transform); - - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - - glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, textures[0]); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width, stream_height, GL_RED, GL_UNSIGNED_BYTE, current_frame_->y); - - glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride/2); - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, textures[1]); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width/2, stream_height/2, GL_RG, GL_UNSIGNED_BYTE, current_frame_->uv); - - glBindVertexArray(frame_vao); - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, nullptr); - glBindVertexArray(0); - - // Reset both texture units - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, 0); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, 0); - glPixelStorei(GL_UNPACK_ALIGNMENT, 4); - glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - - shader_program_->release(); -} - -void CameraWidget::vipcConnected(VisionIpcClient *vipc_client) { - makeCurrent(); - stream_width = vipc_client->buffers[0].width; - stream_height = vipc_client->buffers[0].height; - stream_stride = vipc_client->buffers[0].stride; - - glBindTexture(GL_TEXTURE_2D, textures[0]); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, stream_width, stream_height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); - assert(glGetError() == GL_NO_ERROR); - - glBindTexture(GL_TEXTURE_2D, textures[1]); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, stream_width/2, stream_height/2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr); - assert(glGetError() == GL_NO_ERROR); -} - -void CameraWidget::vipcFrameReceived() { - update(); -} - -void CameraWidget::vipcThread() { - VisionStreamType cur_stream = requested_stream_type; - std::unique_ptr vipc_client; - VisionIpcBufExtra frame_meta = {}; - - while (!QThread::currentThread()->isInterruptionRequested()) { - if (!vipc_client || cur_stream != requested_stream_type) { - clearFrames(); - qDebug().nospace() << "connecting to stream " << requested_stream_type << ", was connected to " << cur_stream; - cur_stream = requested_stream_type; - vipc_client.reset(new VisionIpcClient(stream_name, cur_stream, false)); - } - active_stream_type = cur_stream; - - if (!vipc_client->connected) { - clearFrames(); - auto streams = VisionIpcClient::getAvailableStreams(stream_name, false); - if (streams.empty()) { - QThread::msleep(100); - continue; - } - emit vipcAvailableStreamsUpdated(streams); - - if (!vipc_client->connect(false)) { - QThread::msleep(100); - continue; - } - emit vipcThreadConnected(vipc_client.get()); - } - - if (VisionBuf *buf = vipc_client->recv(&frame_meta, 100)) { - { - std::lock_guard lk(frame_lock); - current_frame_ = buf; - frame_meta_ = frame_meta; - } - emit vipcThreadFrameReceived(); - } - } -} - -void CameraWidget::clearFrames() { - std::lock_guard lk(frame_lock); - current_frame_ = nullptr; - available_streams.clear(); -} diff --git a/tools/cabana/chart/chart.cc b/tools/cabana/chart/chart.cc deleted file mode 100644 index 9dfdc595f0..0000000000 --- a/tools/cabana/chart/chart.cc +++ /dev/null @@ -1,862 +0,0 @@ -#include "tools/cabana/chart/chart.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/chart/chartswidget.h" - -// ChartAxisElement's padding is 4 (https://codebrowser.dev/qt5/qtcharts/src/charts/axis/chartaxiselement_p.h.html) -const int AXIS_X_TOP_MARGIN = 4; -const double MIN_ZOOM_SECONDS = 0.01; // 10ms -// Define a small value of epsilon to compare double values -const float EPSILON = 0.000001; -static inline bool xLessThan(const QPointF &p, float x) { return p.x() < (x - EPSILON); } - -ChartView::ChartView(const std::pair &x_range, ChartsWidget *parent) - : charts_widget(parent), QChartView(parent) { - series_type = (SeriesType)settings.chart_series_type; - chart()->setBackgroundVisible(false); - axis_x = new QValueAxis(this); - axis_y = new QValueAxis(this); - chart()->addAxis(axis_x, Qt::AlignBottom); - chart()->addAxis(axis_y, Qt::AlignLeft); - chart()->legend()->layout()->setContentsMargins(0, 0, 0, 0); - chart()->legend()->setShowToolTips(true); - chart()->setMargins({0, 0, 0, 0}); - - axis_x->setRange(x_range.first, x_range.second); - - tip_label = new TipLabel(this); - createToolButtons(); - setRubberBand(QChartView::HorizontalRubberBand); - setMouseTracking(true); - setTheme(utils::isDarkTheme() ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight); - signal_value_font.setPointSize(9); - - QObject::connect(axis_y, &QValueAxis::rangeChanged, this, &ChartView::resetChartCache); - QObject::connect(axis_y, &QAbstractAxis::titleTextChanged, this, &ChartView::resetChartCache); - QObject::connect(window()->windowHandle(), &QWindow::screenChanged, this, &ChartView::resetChartCache); - - QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartView::signalRemoved); - QObject::connect(dbc(), &DBCManager::signalUpdated, this, &ChartView::signalUpdated); - QObject::connect(dbc(), &DBCManager::msgRemoved, this, &ChartView::msgRemoved); - QObject::connect(dbc(), &DBCManager::msgUpdated, this, &ChartView::msgUpdated); -} - -void ChartView::createToolButtons() { - move_icon = new QGraphicsPixmapItem(utils::icon("grip-horizontal"), chart()); - move_icon->setToolTip(tr("Drag and drop to move chart")); - - QToolButton *remove_btn = new ToolButton("x", tr("Remove Chart")); - close_btn_proxy = new QGraphicsProxyWidget(chart()); - close_btn_proxy->setWidget(remove_btn); - close_btn_proxy->setZValue(chart()->zValue() + 11); - - menu = new QMenu(this); - // series types - auto change_series_group = new QActionGroup(menu); - change_series_group->setExclusive(true); - QStringList types{tr("Line"), tr("Step Line"), tr("Scatter")}; - for (int i = 0; i < types.size(); ++i) { - QAction *act = new QAction(types[i], change_series_group); - act->setData(i); - act->setCheckable(true); - act->setChecked(i == (int)series_type); - menu->addAction(act); - } - menu->addSeparator(); - menu->addAction(tr("Manage Signals"), this, &ChartView::manageSignals); - split_chart_act = menu->addAction(tr("Split Chart"), [this]() { charts_widget->splitChart(this); }); - - QToolButton *manage_btn = new ToolButton("list", ""); - manage_btn->setMenu(menu); - manage_btn->setPopupMode(QToolButton::InstantPopup); - manage_btn->setStyleSheet("QToolButton::menu-indicator { image: none; }"); - manage_btn_proxy = new QGraphicsProxyWidget(chart()); - manage_btn_proxy->setWidget(manage_btn); - manage_btn_proxy->setZValue(chart()->zValue() + 11); - - close_act = new QAction(tr("Close"), this); - QObject::connect(close_act, &QAction::triggered, [this] () { charts_widget->removeChart(this); }); - QObject::connect(remove_btn, &QToolButton::clicked, close_act, &QAction::triggered); - QObject::connect(change_series_group, &QActionGroup::triggered, [this](QAction *action) { - setSeriesType((SeriesType)action->data().toInt()); - }); -} - -QSize ChartView::sizeHint() const { - return {CHART_MIN_WIDTH, settings.chart_height}; -} - -void ChartView::setTheme(QChart::ChartTheme theme) { - chart()->setTheme(theme); - if (theme == QChart::ChartThemeDark) { - axis_x->setTitleBrush(palette().text()); - axis_x->setLabelsBrush(palette().text()); - axis_y->setTitleBrush(palette().text()); - axis_y->setLabelsBrush(palette().text()); - chart()->legend()->setLabelColor(palette().color(QPalette::Text)); - } - axis_x->setLineVisible(false); - axis_y->setLineVisible(false); - for (auto &s : sigs) { - s.series->setColor(s.sig->color); - } -} - -void ChartView::addSignal(const MessageId &msg_id, const cabana::Signal *sig) { - if (hasSignal(msg_id, sig)) return; - - QXYSeries *series = createSeries(series_type, sig->color); - sigs.push_back({.msg_id = msg_id, .sig = sig, .series = series}); - updateSeries(sig); - updateSeriesPoints(); - updateTitle(); - emit charts_widget->seriesChanged(); -} - -bool ChartView::hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const { - return std::any_of(sigs.cbegin(), sigs.cend(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; }); -} - -void ChartView::removeIf(std::function predicate) { - int prev_size = sigs.size(); - for (auto it = sigs.begin(); it != sigs.end(); /**/) { - if (predicate(*it)) { - chart()->removeSeries(it->series); - it->series->deleteLater(); - it = sigs.erase(it); - } else { - ++it; - } - } - if (sigs.empty()) { - charts_widget->removeChart(this); - } else if (sigs.size() != prev_size) { - emit charts_widget->seriesChanged(); - updateAxisY(); - resetChartCache(); - } -} - -void ChartView::signalUpdated(const cabana::Signal *sig) { - auto it = std::find_if(sigs.begin(), sigs.end(), [sig](auto &s) { return s.sig == sig; }); - if (it != sigs.end()) { - if (it->series->color() != sig->color) { - setSeriesColor(it->series, sig->color); - } - updateTitle(); - updateSeries(sig); - } -} - -void ChartView::msgUpdated(MessageId id) { - if (std::any_of(sigs.cbegin(), sigs.cend(), [=](auto &s) { return s.msg_id.address == id.address; })) { - updateTitle(); - } -} - -void ChartView::manageSignals() { - SignalSelector dlg(tr("Manage Chart"), this); - for (auto &s : sigs) { - dlg.addSelected(s.msg_id, s.sig); - } - if (dlg.exec() == QDialog::Accepted) { - auto items = dlg.seletedItems(); - for (auto s : items) { - addSignal(s->msg_id, s->sig); - } - removeIf([&](auto &s) { - return std::none_of(items.cbegin(), items.cend(), [&](auto &it) { return s.msg_id == it->msg_id && s.sig == it->sig; }); - }); - } -} - -void ChartView::resizeEvent(QResizeEvent *event) { - qreal left, top, right, bottom; - chart()->layout()->getContentsMargins(&left, &top, &right, &bottom); - move_icon->setPos(left, top); - close_btn_proxy->setPos(rect().right() - right - close_btn_proxy->size().width(), top); - int x = close_btn_proxy->pos().x() - manage_btn_proxy->size().width() - style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing); - manage_btn_proxy->setPos(x, top); - if (align_to > 0) { - updatePlotArea(align_to, true); - } - QChartView::resizeEvent(event); -} - -void ChartView::updatePlotArea(int left_pos, bool force) { - if (align_to != left_pos || force) { - align_to = left_pos; - - qreal left, top, right, bottom; - chart()->layout()->getContentsMargins(&left, &top, &right, &bottom); - QSizeF legend_size = chart()->legend()->layout()->minimumSize(); - legend_size.setWidth(manage_btn_proxy->sceneBoundingRect().left() - move_icon->sceneBoundingRect().right()); - chart()->legend()->setGeometry({move_icon->sceneBoundingRect().topRight(), legend_size}); - - // add top space for signal value - int adjust_top = chart()->legend()->geometry().height() + QFontMetrics(signal_value_font).height() + 3; - adjust_top = std::max(adjust_top, manage_btn_proxy->sceneBoundingRect().height() + style()->pixelMetric(QStyle::PM_LayoutTopMargin)); - // add right space for x-axis label - QSizeF x_label_size = QFontMetrics(axis_x->labelsFont()).size(Qt::TextSingleLine, QString::number(axis_x->max(), 'f', 2)); - x_label_size += QSizeF{5, 5}; - chart()->setPlotArea(rect().adjusted(align_to + left, adjust_top + top, -x_label_size.width() / 2 - right, -x_label_size.height() - bottom)); - chart()->layout()->invalidate(); - resetChartCache(); - } -} - -void ChartView::updateTitle() { - for (QLegendMarker *marker : chart()->legend()->markers()) { - QObject::connect(marker, &QLegendMarker::clicked, this, &ChartView::handleMarkerClicked, Qt::UniqueConnection); - } - - // Use CSS to draw titles in the WindowText color - auto tmp = palette().color(QPalette::WindowText); - auto titleColorCss = tmp.name(QColor::HexArgb); - // Draw message details in similar color, but slightly fade it to the background - tmp.setAlpha(180); - auto msgColorCss = tmp.name(QColor::HexArgb); - - for (auto &s : sigs) { - auto decoration = s.series->isVisible() ? "none" : "line-through"; - s.series->setName(QString("%3 %5 %6") - .arg(decoration, titleColorCss, QString::fromStdString(s.sig->name), - msgColorCss, QString::fromStdString(msgName(s.msg_id)), QString::fromStdString(s.msg_id.toString()))); - } - split_chart_act->setEnabled(sigs.size() > 1); - resetChartCache(); -} - -void ChartView::updatePlot(double cur, double min, double max) { - cur_sec = cur; - if (min != axis_x->min() || max != axis_x->max()) { - axis_x->setRange(min, max); - updateAxisY(); - updateSeriesPoints(); - // update tooltip - if (tooltip_x >= 0) { - showTip(chart()->mapToValue({tooltip_x, 0}).x()); - } - resetChartCache(); - } - viewport()->update(); -} - -void ChartView::updateSeriesPoints() { - // Show points when zoomed in enough - for (auto &s : sigs) { - auto begin = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan); - auto end = std::lower_bound(begin, s.vals.cend(), axis_x->max(), xLessThan); - if (begin != end) { - int num_points = std::max((end - begin), 1); - QPointF right_pt = end == s.vals.cend() ? s.vals.back() : *end; - double pixels_per_point = (chart()->mapToPosition(right_pt).x() - chart()->mapToPosition(*begin).x()) / num_points; - - if (series_type == SeriesType::Scatter) { - qreal size = std::clamp(pixels_per_point / 2.0, 2.0, 8.0); - if (s.series->useOpenGL()) { - size *= devicePixelRatioF(); - } - ((QScatterSeries *)s.series)->setMarkerSize(size); - } else { - s.series->setPointsVisible(num_points == 1 || pixels_per_point > 20); - } - } - } -} - -void ChartView::appendCanEvents(const cabana::Signal *sig, const std::vector &events, - std::vector &vals, std::vector &step_vals) { - vals.reserve(vals.size() + events.capacity()); - step_vals.reserve(step_vals.size() + events.capacity() * 2); - - double value = 0; - for (const CanEvent *e : events) { - if (sig->getValue(e->dat, e->size, &value)) { - const double ts = can->toSeconds(e->mono_time); - vals.emplace_back(ts, value); - if (!step_vals.empty()) - step_vals.emplace_back(ts, step_vals.back().y()); - step_vals.emplace_back(ts, value); - } - } -} - -void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap *msg_new_events) { - for (auto &s : sigs) { - if (!sig || s.sig == sig) { - if (!msg_new_events) { - s.vals.clear(); - s.step_vals.clear(); - } - auto events = msg_new_events ? msg_new_events : &can->eventsMap(); - auto it = events->find(s.msg_id); - if (it == events->end() || it->second.empty()) continue; - - if (s.vals.empty() || can->toSeconds(it->second.back()->mono_time) > s.vals.back().x()) { - appendCanEvents(s.sig, it->second, s.vals, s.step_vals); - } else { - std::vector vals, step_vals; - appendCanEvents(s.sig, it->second, vals, step_vals); - s.vals.insert(std::lower_bound(s.vals.begin(), s.vals.end(), vals.front().x(), xLessThan), - vals.begin(), vals.end()); - s.step_vals.insert(std::lower_bound(s.step_vals.begin(), s.step_vals.end(), step_vals.front().x(), xLessThan), - step_vals.begin(), step_vals.end()); - } - - if (!can->liveStreaming()) { - s.segment_tree.build(s.vals); - } - const auto &points = series_type == SeriesType::StepLine ? s.step_vals : s.vals; - s.series->replace(QVector(points.cbegin(), points.cend())); - } - } - updateAxisY(); - // invoke resetChartCache in ui thread - QMetaObject::invokeMethod(this, &ChartView::resetChartCache, Qt::QueuedConnection); -} - -// auto zoom on yaxis -void ChartView::updateAxisY() { - if (sigs.empty()) return; - - double min = std::numeric_limits::max(); - double max = std::numeric_limits::lowest(); - QString unit = QString::fromStdString(sigs[0].sig->unit); - - for (auto &s : sigs) { - if (!s.series->isVisible()) continue; - - // Only show unit when all signals have the same unit - if (unit != QString::fromStdString(s.sig->unit)) { - unit.clear(); - } - - auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan); - auto last = std::lower_bound(first, s.vals.cend(), axis_x->max(), xLessThan); - s.min = std::numeric_limits::max(); - s.max = std::numeric_limits::lowest(); - if (can->liveStreaming()) { - for (auto it = first; it != last; ++it) { - if (it->y() < s.min) s.min = it->y(); - if (it->y() > s.max) s.max = it->y(); - } - } else { - std::tie(s.min, s.max) = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last)); - } - min = std::min(min, s.min); - max = std::max(max, s.max); - } - if (min == std::numeric_limits::max()) min = 0; - if (max == std::numeric_limits::lowest()) max = 0; - - if (axis_y->titleText() != unit) { - axis_y->setTitleText(unit); - y_label_width = 0; // recalc width - } - - double delta = std::abs(max - min) < 1e-3 ? 1 : (max - min) * 0.05; - auto [min_y, max_y, tick_count] = getNiceAxisNumbers(min - delta, max + delta, 3); - if (min_y != axis_y->min() || max_y != axis_y->max() || y_label_width == 0) { - axis_y->setRange(min_y, max_y); - axis_y->setTickCount(tick_count); - - int n = std::max(int(-std::floor(std::log10((max_y - min_y) / (tick_count - 1)))), 0); - int max_label_width = 0; - QFontMetrics fm(axis_y->labelsFont()); - for (int i = 0; i < tick_count; i++) { - qreal value = min_y + (i * (max_y - min_y) / (tick_count - 1)); - max_label_width = std::max(max_label_width, fm.horizontalAdvance(QString::number(value, 'f', n))); - } - - int title_spacing = unit.isEmpty() ? 0 : QFontMetrics(axis_y->titleFont()).size(Qt::TextSingleLine, unit).height(); - y_label_width = title_spacing + max_label_width + 15; - axis_y->setLabelFormat(QString("%.%1f").arg(n)); - emit axisYLabelWidthChanged(y_label_width); - } -} - -std::tuple ChartView::getNiceAxisNumbers(qreal min, qreal max, int tick_count) { - qreal range = niceNumber((max - min), true); // range with ceiling - qreal step = niceNumber(range / (tick_count - 1), false); - min = std::floor(min / step); - max = std::ceil(max / step); - tick_count = int(max - min) + 1; - return {min * step, max * step, tick_count}; -} - -// nice numbers can be expressed as form of 1*10^n, 2* 10^n or 5*10^n -qreal ChartView::niceNumber(qreal x, bool ceiling) { - qreal z = std::pow(10, std::floor(std::log10(x))); //find corresponding number of the form of 10^n than is smaller than x - qreal q = x / z; //q<10 && q>=1; - if (ceiling) { - if (q <= 1.0) q = 1; - else if (q <= 2.0) q = 2; - else if (q <= 5.0) q = 5; - else q = 10; - } else { - if (q < 1.5) q = 1; - else if (q < 3.0) q = 2; - else if (q < 7.0) q = 5; - else q = 10; - } - return q * z; -} - -QPixmap getBlankShadowPixmap(const QPixmap &px, int radius) { - QGraphicsDropShadowEffect *e = new QGraphicsDropShadowEffect; - e->setColor(QColor(40, 40, 40, 245)); - e->setOffset(0, 0); - e->setBlurRadius(radius); - - qreal dpr = px.devicePixelRatio(); - QPixmap blank(px.size()); - blank.setDevicePixelRatio(dpr); - blank.fill(Qt::white); - - QGraphicsScene scene; - QGraphicsPixmapItem item(blank); - item.setGraphicsEffect(e); - scene.addItem(&item); - - QPixmap shadow(px.size() + QSize(radius * dpr * 2, radius * dpr * 2)); - shadow.setDevicePixelRatio(dpr); - shadow.fill(Qt::transparent); - QPainter p(&shadow); - scene.render(&p, {QPoint(), shadow.size() / dpr}, item.boundingRect().adjusted(-radius, -radius, radius, radius)); - return shadow; -} - -static QPixmap getDropPixmap(const QPixmap &src) { - static QPixmap shadow_px; - const int radius = 10; - if (shadow_px.size() != src.size() + QSize(radius * 2, radius * 2)) { - shadow_px = getBlankShadowPixmap(src, radius); - } - QPixmap px = shadow_px; - QPainter p(&px); - QRectF target_rect(QPointF(radius, radius), src.size() / src.devicePixelRatio()); - p.drawPixmap(target_rect.topLeft(), src); - p.setCompositionMode(QPainter::CompositionMode_DestinationIn); - p.fillRect(target_rect, QColor(0, 0, 0, 200)); - return px; -} - -void ChartView::contextMenuEvent(QContextMenuEvent *event) { - QMenu context_menu(this); - context_menu.addActions(menu->actions()); - context_menu.addSeparator(); - context_menu.addAction(charts_widget->undo_zoom_action); - context_menu.addAction(charts_widget->redo_zoom_action); - context_menu.addSeparator(); - context_menu.addAction(close_act); - context_menu.exec(event->globalPos()); -} - -void ChartView::mousePressEvent(QMouseEvent *event) { - if (event->button() == Qt::LeftButton && move_icon->sceneBoundingRect().contains(event->pos())) { - QMimeData *mimeData = new QMimeData; - mimeData->setData(CHART_MIME_TYPE, QByteArray::number((qulonglong)this)); - QPixmap px = grab().scaledToWidth(CHART_MIN_WIDTH * viewport()->devicePixelRatio(), Qt::SmoothTransformation); - charts_widget->stopAutoScroll(); - QDrag *drag = new QDrag(this); - drag->setMimeData(mimeData); - drag->setPixmap(getDropPixmap(px)); - drag->setHotSpot(-QPoint(5, 5)); - drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::MoveAction); - } else if (event->button() == Qt::LeftButton && QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) { - // Save current playback state when scrubbing - resume_after_scrub = !can->isPaused(); - if (resume_after_scrub) { - can->pause(true); - } - is_scrubbing = true; - } else { - QChartView::mousePressEvent(event); - } -} - -void ChartView::mouseReleaseEvent(QMouseEvent *event) { - auto rubber = findChild(); - if (event->button() == Qt::LeftButton && rubber && rubber->isVisible()) { - rubber->hide(); - auto rect = rubber->geometry().normalized(); - // Prevent zooming/seeking past the end of the route - double min = std::clamp(chart()->mapToValue(rect.topLeft()).x(), can->minSeconds(), can->maxSeconds()); - double max = std::clamp(chart()->mapToValue(rect.bottomRight()).x(), can->minSeconds(), can->maxSeconds()); - if (rubber->width() <= 0) { - // no rubber dragged, seek to mouse position - can->seekTo(min); - } else if (rubber->width() > 10 && (max - min) > MIN_ZOOM_SECONDS) { - charts_widget->zoom_undo_stack->push(new ZoomCommand({min, max})); - } else { - viewport()->update(); - } - event->accept(); - } else if (event->button() == Qt::RightButton) { - charts_widget->zoom_undo_stack->undo(); - event->accept(); - } else { - QGraphicsView::mouseReleaseEvent(event); - } - - // Resume playback if we were scrubbing - is_scrubbing = false; - if (resume_after_scrub) { - can->pause(false); - resume_after_scrub = false; - } -} - -void ChartView::mouseMoveEvent(QMouseEvent *ev) { - const auto plot_area = chart()->plotArea(); - // Scrubbing - if (is_scrubbing && QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) { - if (plot_area.contains(ev->pos())) { - can->seekTo(std::clamp(chart()->mapToValue(ev->pos()).x(), can->minSeconds(), can->maxSeconds())); - } - } - - auto rubber = findChild(); - bool is_zooming = rubber && rubber->isVisible(); - clearTrackPoints(); - - if (!is_zooming && plot_area.contains(ev->pos()) && isActiveWindow()) { - charts_widget->showValueTip(secondsAtPoint(ev->pos())); - } else if (tip_label->isVisible()) { - charts_widget->showValueTip(-1); - } - - QChartView::mouseMoveEvent(ev); - if (is_zooming) { - QRect rubber_rect = rubber->geometry(); - rubber_rect.setLeft(std::max(rubber_rect.left(), (int)plot_area.left())); - rubber_rect.setRight(std::min(rubber_rect.right(), (int)plot_area.right())); - if (rubber_rect != rubber->geometry()) { - rubber->setGeometry(rubber_rect); - } - viewport()->update(); - } -} - -void ChartView::showTip(double sec) { - QRect tip_area(0, chart()->plotArea().top(), rect().width(), chart()->plotArea().height()); - QRect visible_rect = charts_widget->chartVisibleRect(this).intersected(tip_area); - if (visible_rect.isEmpty()) { - tip_label->hide(); - return; - } - - tooltip_x = chart()->mapToPosition({sec, 0}).x(); - qreal x = -1; - QStringList text_list; - for (auto &s : sigs) { - if (s.series->isVisible()) { - QString value = "--"; - // use reverse iterator to find last item <= sec. - auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double v) { return p.x() > v; }); - if (it != s.vals.crend() && it->x() >= axis_x->min()) { - value = QString::fromStdString(s.sig->formatValue(it->y(), false)); - s.track_pt = *it; - x = std::max(x, chart()->mapToPosition(*it).x()); - } - QString name = sigs.size() > 1 ? QString::fromStdString(s.sig->name) + ": " : ""; - QString min = s.min == std::numeric_limits::max() ? "--" : QString::number(s.min); - QString max = s.max == std::numeric_limits::lowest() ? "--" : QString::number(s.max); - text_list << QString("%2%3 (%4, %5)") - .arg(s.series->color().name(), name, value, min, max); - } - } - if (x < 0) { - x = tooltip_x; - } - QPoint pt(x, chart()->plotArea().top()); - text_list.push_front(QString::number(chart()->mapToValue({x, 0}).x(), 'f', 3)); - QString text = "

" % text_list.join("
") % "

"; - tip_label->showText(pt, text, this, visible_rect); - viewport()->update(); -} - -void ChartView::hideTip() { - clearTrackPoints(); - tooltip_x = -1; - tip_label->hide(); - viewport()->update(); -} - -void ChartView::dragEnterEvent(QDragEnterEvent *event) { - if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { - drawDropIndicator(event->source() != this); - event->acceptProposedAction(); - } -} - -void ChartView::dragMoveEvent(QDragMoveEvent *event) { - if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { - event->setDropAction(event->source() == this ? Qt::MoveAction : Qt::CopyAction); - event->accept(); - } - charts_widget->startAutoScroll(); -} - -void ChartView::dropEvent(QDropEvent *event) { - if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { - if (event->source() != this) { - ChartView *source_chart = (ChartView *)event->source(); - for (auto &s : source_chart->sigs) { - source_chart->chart()->removeSeries(s.series); - addSeries(s.series); - } - sigs.insert(sigs.end(), std::move_iterator(source_chart->sigs.begin()), std::move_iterator(source_chart->sigs.end())); - updateAxisY(); - updateTitle(); - startAnimation(); - - source_chart->sigs.clear(); - charts_widget->removeChart(source_chart); - event->acceptProposedAction(); - } - can_drop = false; - } -} - -void ChartView::resetChartCache() { - chart_pixmap = QPixmap(); - viewport()->update(); -} - -void ChartView::startAnimation() { - QGraphicsOpacityEffect *eff = new QGraphicsOpacityEffect(this); - viewport()->setGraphicsEffect(eff); - QPropertyAnimation *a = new QPropertyAnimation(eff, "opacity"); - a->setDuration(250); - a->setStartValue(0.3); - a->setEndValue(1); - a->setEasingCurve(QEasingCurve::InBack); - a->start(QPropertyAnimation::DeleteWhenStopped); -} - -void ChartView::paintEvent(QPaintEvent *event) { - if (!can->liveStreaming()) { - if (chart_pixmap.isNull()) { - const qreal dpr = viewport()->devicePixelRatioF(); - chart_pixmap = QPixmap(viewport()->size() * dpr); - chart_pixmap.setDevicePixelRatio(dpr); - QPainter p(&chart_pixmap); - p.setRenderHints(QPainter::Antialiasing); - drawBackground(&p, viewport()->rect()); - scene()->setSceneRect(viewport()->rect()); - scene()->render(&p, viewport()->rect()); - } - - QPainter painter(viewport()); - painter.setRenderHints(QPainter::Antialiasing); - painter.drawPixmap(QPoint(), chart_pixmap); - if (can_drop) { - painter.setPen(QPen(palette().color(QPalette::Highlight), 4)); - painter.drawRect(viewport()->rect()); - } - QRectF exposed_rect = mapToScene(event->region().boundingRect()).boundingRect(); - drawForeground(&painter, exposed_rect); - } else { - QChartView::paintEvent(event); - } -} - -void ChartView::drawBackground(QPainter *painter, const QRectF &rect) { - painter->fillRect(rect, palette().color(QPalette::Base)); -} - -void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { - drawTimeline(painter); - drawSignalValue(painter); - // draw track points - painter->setPen(Qt::NoPen); - qreal track_line_x = -1; - for (auto &s : sigs) { - if (!s.track_pt.isNull() && s.series->isVisible()) { - painter->setBrush(s.series->color().darker(125)); - QPointF pos = chart()->mapToPosition(s.track_pt); - painter->drawEllipse(pos, 5.5, 5.5); - track_line_x = std::max(track_line_x, pos.x()); - } - } - if (track_line_x > 0) { - auto plot_area = chart()->plotArea(); - painter->setPen(QPen(Qt::darkGray, 1, Qt::DashLine)); - painter->drawLine(QPointF{track_line_x, plot_area.top()}, QPointF{track_line_x, plot_area.bottom()}); - } - - // paint points. OpenGL mode lacks certain features (such as showing points) - painter->setPen(Qt::NoPen); - for (auto &s : sigs) { - if (s.series->useOpenGL() && s.series->isVisible() && s.series->pointsVisible()) { - auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan); - auto last = std::lower_bound(first, s.vals.cend(), axis_x->max(), xLessThan); - painter->setBrush(s.series->color()); - for (auto it = first; it != last; ++it) { - painter->drawEllipse(chart()->mapToPosition(*it), 4, 4); - } - } - } - - drawRubberBandTimeRange(painter); -} - -void ChartView::drawRubberBandTimeRange(QPainter *painter) { - auto rubber = findChild(); - if (rubber && rubber->isVisible() && rubber->width() > 1) { - painter->setPen(Qt::white); - auto rubber_rect = rubber->geometry().normalized(); - for (const auto &pt : {rubber_rect.bottomLeft(), rubber_rect.bottomRight()}) { - QString sec = QString::number(chart()->mapToValue(pt).x(), 'f', 2); - auto r = painter->fontMetrics().boundingRect(sec).adjusted(-6, -AXIS_X_TOP_MARGIN, 6, AXIS_X_TOP_MARGIN); - pt == rubber_rect.bottomLeft() ? r.moveTopRight(pt + QPoint{0, 2}) : r.moveTopLeft(pt + QPoint{0, 2}); - painter->fillRect(r, Qt::gray); - painter->drawText(r, Qt::AlignCenter, sec); - } - } -} - -void ChartView::drawTimeline(QPainter *painter) { - const auto plot_area = chart()->plotArea(); - // draw vertical time line - qreal x = std::clamp(chart()->mapToPosition(QPointF{cur_sec, 0}).x(), plot_area.left(), plot_area.right()); - painter->setPen(QPen(chart()->titleBrush().color(), 1)); - painter->drawLine(QPointF{x, plot_area.top() - 1}, QPointF{x, plot_area.bottom() + 1}); - - // draw current time under the axis-x - QString time_str = QString::number(cur_sec, 'f', 2); - QSize time_str_size = QFontMetrics(axis_x->labelsFont()).size(Qt::TextSingleLine, time_str) + QSize(8, 2); - QRectF time_str_rect(QPointF(x - time_str_size.width() / 2.0, plot_area.bottom() + AXIS_X_TOP_MARGIN), time_str_size); - QPainterPath path; - path.addRoundedRect(time_str_rect, 3, 3); - painter->fillPath(path, utils::isDarkTheme() ? Qt::darkGray : Qt::gray); - painter->setPen(palette().color(QPalette::BrightText)); - painter->setFont(axis_x->labelsFont()); - painter->drawText(time_str_rect, Qt::AlignCenter, time_str); -} - -void ChartView::drawSignalValue(QPainter *painter) { - auto item_group = qgraphicsitem_cast(chart()->legend()->childItems()[0]); - assert(item_group != nullptr); - auto legend_markers = item_group->childItems(); - assert(legend_markers.size() == sigs.size()); - - painter->setFont(signal_value_font); - painter->setPen(chart()->legend()->labelColor()); - int i = 0; - for (auto &s : sigs) { - auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), cur_sec, - [](auto &p, double x) { return p.x() > x + EPSILON; }); - QString value = (it != s.vals.crend() && it->x() >= axis_x->min()) ? QString::fromStdString(s.sig->formatValue(it->y())) : "--"; - QRectF marker_rect = legend_markers[i++]->sceneBoundingRect(); - QRectF value_rect(marker_rect.bottomLeft() - QPoint(0, 1), marker_rect.size()); - QString elided_val = painter->fontMetrics().elidedText(value, Qt::ElideRight, value_rect.width()); - painter->drawText(value_rect, Qt::AlignHCenter | Qt::AlignTop, elided_val); - } -} - -QXYSeries *ChartView::createSeries(SeriesType type, QColor color) { - QXYSeries *series = nullptr; - if (type == SeriesType::Line) { - series = new QLineSeries(this); - chart()->legend()->setMarkerShape(QLegend::MarkerShapeRectangle); - } else if (type == SeriesType::StepLine) { - series = new QLineSeries(this); - chart()->legend()->setMarkerShape(QLegend::MarkerShapeFromSeries); - } else { - series = new QScatterSeries(this); - static_cast(series)->setBorderColor(color); - chart()->legend()->setMarkerShape(QLegend::MarkerShapeCircle); - } - series->setColor(color); - // TODO: Due to a bug in CameraWidget the camera frames - // are drawn instead of the graphs on MacOS. Re-enable OpenGL when fixed -#ifndef __APPLE__ - series->setUseOpenGL(true); - // Qt doesn't properly apply device pixel ratio in OpenGL mode - QPen pen = series->pen(); - pen.setWidthF(2.0 * devicePixelRatioF()); - series->setPen(pen); -#endif - addSeries(series); - return series; -} - -void ChartView::addSeries(QXYSeries *series) { - setSeriesColor(series, series->color()); - chart()->addSeries(series); - series->attachAxis(axis_x); - series->attachAxis(axis_y); - - // disables the delivery of mouse events to the opengl widget. - // this enables the user to select the zoom area when the mouse press on the data point. - auto glwidget = findChild(); - if (glwidget && !glwidget->testAttribute(Qt::WA_TransparentForMouseEvents)) { - glwidget->setAttribute(Qt::WA_TransparentForMouseEvents); - } -} - -void ChartView::setSeriesColor(QXYSeries *series, QColor color) { - auto existing_series = chart()->series(); - for (auto s : existing_series) { - if (s != series && std::abs(color.hueF() - qobject_cast(s)->color().hueF()) < 0.1) { - // use different color to distinguish it from others. - auto last_color = qobject_cast(existing_series.back())->color(); - color.setHsvF(std::fmod(last_color.hueF() + 60 / 360.0, 1.0), - QRandomGenerator::global()->bounded(35, 100) / 100.0, - QRandomGenerator::global()->bounded(85, 100) / 100.0); - break; - } - } - series->setColor(color); -} - -void ChartView::setSeriesType(SeriesType type) { - if (type != series_type) { - series_type = type; - for (auto &s : sigs) { - chart()->removeSeries(s.series); - s.series->deleteLater(); - } - for (auto &s : sigs) { - s.series = createSeries(series_type, s.sig->color); - const auto &points = series_type == SeriesType::StepLine ? s.step_vals : s.vals; - s.series->replace(QVector(points.cbegin(), points.cend())); - } - updateSeriesPoints(); - updateTitle(); - - menu->actions()[(int)type]->setChecked(true); - } -} - -void ChartView::handleMarkerClicked() { - auto marker = qobject_cast(sender()); - Q_ASSERT(marker); - if (sigs.size() > 1) { - auto series = marker->series(); - series->setVisible(!series->isVisible()); - marker->setVisible(true); - updateAxisY(); - updateTitle(); - } -} diff --git a/tools/cabana/commands.h b/tools/cabana/commands.h deleted file mode 100644 index 4081f86985..0000000000 --- a/tools/cabana/commands.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include - -#include "tools/cabana/dbc/dbcmanager.h" -#include "tools/cabana/streams/abstractstream.h" - -class EditMsgCommand : public QUndoCommand { -public: - EditMsgCommand(const MessageId &id, const std::string &name, int size, const std::string &node, - const std::string &comment, QUndoCommand *parent = nullptr); - void undo() override; - void redo() override; - -private: - const MessageId id; - std::string old_name, new_name, old_comment, new_comment, old_node, new_node; - int old_size = 0, new_size = 0; -}; - -class RemoveMsgCommand : public QUndoCommand { -public: - RemoveMsgCommand(const MessageId &id, QUndoCommand *parent = nullptr); - void undo() override; - void redo() override; - -private: - const MessageId id; - cabana::Msg message; -}; - -class AddSigCommand : public QUndoCommand { -public: - AddSigCommand(const MessageId &id, const cabana::Signal &sig, QUndoCommand *parent = nullptr); - void undo() override; - void redo() override; - -private: - const MessageId id; - bool msg_created = false; - cabana::Signal signal = {}; -}; - -class RemoveSigCommand : public QUndoCommand { -public: - RemoveSigCommand(const MessageId &id, const cabana::Signal *sig, QUndoCommand *parent = nullptr); - void undo() override; - void redo() override; - -private: - const MessageId id; - std::vector sigs; -}; - -class EditSignalCommand : public QUndoCommand { -public: - EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig, QUndoCommand *parent = nullptr); - void undo() override; - void redo() override; - -private: - const MessageId id; - std::vector> sigs; // {old_sig, new_sig} -}; - -namespace UndoStack { - QUndoStack *instance(); - inline void push(QUndoCommand *cmd) { instance()->push(cmd); } -}; diff --git a/tools/cabana/dbc/dbcfile.cc b/tools/cabana/dbc/dbcfile.cc deleted file mode 100644 index d9c129ee81..0000000000 --- a/tools/cabana/dbc/dbcfile.cc +++ /dev/null @@ -1,281 +0,0 @@ -#include "tools/cabana/dbc/dbcfile.h" - -#include -#include -#include -#include - -DBCFile::DBCFile(const std::string &dbc_file_name) { - QFile file(QString::fromStdString(dbc_file_name)); - if (file.open(QIODevice::ReadOnly)) { - name_ = QFileInfo(QString::fromStdString(dbc_file_name)).baseName().toStdString(); - filename = dbc_file_name; - parse(file.readAll()); - } else { - throw std::runtime_error("Failed to open file."); - } -} - -DBCFile::DBCFile(const std::string &name, const std::string &content) : name_(name), filename("") { - parse(QString::fromStdString(content)); -} - -bool DBCFile::save() { - assert(!filename.empty()); - return writeContents(filename); -} - -bool DBCFile::saveAs(const std::string &new_filename) { - filename = new_filename; - return save(); -} - -bool DBCFile::writeContents(const std::string &fn) { - QFile file(QString::fromStdString(fn)); - if (file.open(QIODevice::WriteOnly)) { - std::string content = generateDBC(); - return file.write(content.c_str(), content.size()) >= 0; - } - return false; -} - -void DBCFile::updateMsg(const MessageId &id, const std::string &name, uint32_t size, const std::string &node, const std::string &comment) { - auto &m = msgs[id.address]; - m.address = id.address; - m.name = name; - m.size = size; - m.transmitter = node.empty() ? DEFAULT_NODE_NAME : node; - m.comment = comment; -} - -cabana::Msg *DBCFile::msg(uint32_t address) { - auto it = msgs.find(address); - return it != msgs.end() ? &it->second : nullptr; -} - -cabana::Msg *DBCFile::msg(const std::string &name) { - auto it = std::find_if(msgs.begin(), msgs.end(), [&name](auto &m) { return m.second.name == name; }); - return it != msgs.end() ? &(it->second) : nullptr; -} - -cabana::Signal *DBCFile::signal(uint32_t address, const std::string &name) { - auto m = msg(address); - return m ? (cabana::Signal *)m->sig(name) : nullptr; -} - -void DBCFile::parse(const QString &content) { - msgs.clear(); - - int line_num = 0; - QString line; - cabana::Msg *current_msg = nullptr; - int multiplexor_cnt = 0; - bool seen_first = false; - QTextStream stream((QString *)&content); - - while (!stream.atEnd()) { - ++line_num; - QString raw_line = stream.readLine(); - line = raw_line.trimmed(); - - bool seen = true; - try { - if (line.startsWith("BO_ ")) { - multiplexor_cnt = 0; - current_msg = parseBO(line); - } else if (line.startsWith("SG_ ")) { - parseSG(line, current_msg, multiplexor_cnt); - } else if (line.startsWith("VAL_ ")) { - parseVAL(line); - } else if (line.startsWith("CM_ BO_")) { - parseCM_BO(line, content, raw_line, stream); - } else if (line.startsWith("CM_ SG_ ")) { - parseCM_SG(line, content, raw_line, stream); - } else { - seen = false; - } - } catch (std::exception &e) { - throw std::runtime_error(QString("[%1:%2]%3: %4").arg(QString::fromStdString(filename)).arg(line_num).arg(e.what()).arg(line).toStdString()); - } - - if (seen) { - seen_first = true; - } else if (!seen_first) { - header += raw_line.toStdString() + "\n"; - } - } - - for (auto &[_, m] : msgs) { - m.update(); - } -} - -cabana::Msg *DBCFile::parseBO(const QString &line) { - static QRegularExpression bo_regexp(R"(^BO_ (?
\w+) (?\w+) *: (?\w+) (?\w+))"); - - QRegularExpressionMatch match = bo_regexp.match(line); - if (!match.hasMatch()) - throw std::runtime_error("Invalid BO_ line format"); - - uint32_t address = match.captured("address").toUInt(); - if (msgs.count(address) > 0) - throw std::runtime_error(QString("Duplicate message address: %1").arg(address).toStdString()); - - // Create a new message object - cabana::Msg *msg = &msgs[address]; - msg->address = address; - msg->name = match.captured("name").toStdString(); - msg->size = match.captured("size").toULong(); - msg->transmitter = match.captured("transmitter").trimmed().toStdString(); - return msg; -} - -void DBCFile::parseCM_BO(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream) { - static QRegularExpression msg_comment_regexp(R"(^CM_ BO_ *(?
\w+) *\"(?(?:[^"\\]|\\.)*)\"\s*;)"); - - QString parse_line = line; - if (!parse_line.endsWith("\";")) { - int pos = stream.pos() - raw_line.length() - 1; - parse_line = content.mid(pos, content.indexOf("\";", pos)); - } - auto match = msg_comment_regexp.match(parse_line); - if (!match.hasMatch()) - throw std::runtime_error("Invalid message comment format"); - - if (auto m = (cabana::Msg *)msg(match.captured("address").toUInt())) - m->comment = match.captured("comment").trimmed().replace("\\\"", "\"").toStdString(); -} - -void DBCFile::parseSG(const QString &line, cabana::Msg *current_msg, int &multiplexor_cnt) { - static QRegularExpression sg_regexp(R"(^SG_ (\w+) *: (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*))"); - static QRegularExpression sgm_regexp(R"(^SG_ (\w+) (\w+) *: (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*))"); - - if (!current_msg) - throw std::runtime_error("No Message"); - - int offset = 0; - auto match = sg_regexp.match(line); - if (!match.hasMatch()) { - match = sgm_regexp.match(line); - offset = 1; - } - if (!match.hasMatch()) - throw std::runtime_error("Invalid SG_ line format"); - - std::string name = match.captured(1).toStdString(); - if (current_msg->sig(name) != nullptr) - throw std::runtime_error("Duplicate signal name"); - - cabana::Signal s{}; - if (offset == 1) { - auto indicator = match.captured(2); - if (indicator == "M") { - ++multiplexor_cnt; - // Only one signal within a single message can be the multiplexer switch. - if (multiplexor_cnt >= 2) - throw std::runtime_error("Multiple multiplexor"); - - s.type = cabana::Signal::Type::Multiplexor; - } else { - s.type = cabana::Signal::Type::Multiplexed; - s.multiplex_value = indicator.mid(1).toInt(); - } - } - s.name = name; - s.start_bit = match.captured(offset + 2).toInt(); - s.size = match.captured(offset + 3).toInt(); - s.is_little_endian = match.captured(offset + 4).toInt() == 1; - s.is_signed = match.captured(offset + 5) == "-"; - s.factor = match.captured(offset + 6).toDouble(); - s.offset = match.captured(offset + 7).toDouble(); - s.min = match.captured(8 + offset).toDouble(); - s.max = match.captured(9 + offset).toDouble(); - s.unit = match.captured(10 + offset).toStdString(); - s.receiver_name = match.captured(11 + offset).trimmed().toStdString(); - current_msg->sigs.push_back(new cabana::Signal(s)); -} - -void DBCFile::parseCM_SG(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream) { - static QRegularExpression sg_comment_regexp(R"(^CM_ SG_ *(\w+) *(\w+) *\"((?:[^"\\]|\\.)*)\"\s*;)"); - - QString parse_line = line; - if (!parse_line.endsWith("\";")) { - int pos = stream.pos() - raw_line.length() - 1; - parse_line = content.mid(pos, content.indexOf("\";", pos)); - } - auto match = sg_comment_regexp.match(parse_line); - if (!match.hasMatch()) - throw std::runtime_error("Invalid CM_ SG_ line format"); - - if (auto s = signal(match.captured(1).toUInt(), match.captured(2).toStdString())) { - s->comment = match.captured(3).trimmed().replace("\\\"", "\"").toStdString(); - } -} - -void DBCFile::parseVAL(const QString &line) { - static QRegularExpression val_regexp(R"(VAL_ (\w+) (\w+) (\s*[-+]?[0-9]+\s+\".+?\"[^;]*))"); - - auto match = val_regexp.match(line); - if (!match.hasMatch()) - throw std::runtime_error("invalid VAL_ line format"); - - if (auto s = signal(match.captured(1).toUInt(), match.captured(2).toStdString())) { - QStringList desc_list = match.captured(3).trimmed().split('"'); - for (int i = 0; i < desc_list.size(); i += 2) { - auto val = desc_list[i].trimmed(); - if (!val.isEmpty() && (i + 1) < desc_list.size()) { - auto desc = desc_list[i + 1].trimmed(); - s->val_desc.push_back({val.toDouble(), desc.toStdString()}); - } - } - } -} - -std::string DBCFile::generateDBC() { - std::string dbc_string, comment, val_desc; - for (const auto &[address, m] : msgs) { - const std::string &transmitter = m.transmitter.empty() ? DEFAULT_NODE_NAME : m.transmitter; - dbc_string += "BO_ " + std::to_string(address) + " " + m.name + ": " + std::to_string(m.size) + " " + transmitter + "\n"; - if (!m.comment.empty()) { - std::string escaped_comment = m.comment; - // Replace " with \" - for (size_t pos = 0; (pos = escaped_comment.find('"', pos)) != std::string::npos; pos += 2) - escaped_comment.replace(pos, 1, "\\\""); - comment += "CM_ BO_ " + std::to_string(address) + " \"" + escaped_comment + "\";\n"; - } - for (auto sig : m.getSignals()) { - std::string multiplexer_indicator; - if (sig->type == cabana::Signal::Type::Multiplexor) { - multiplexer_indicator = "M "; - } else if (sig->type == cabana::Signal::Type::Multiplexed) { - multiplexer_indicator = "m" + std::to_string(sig->multiplex_value) + " "; - } - const std::string &recv = sig->receiver_name.empty() ? DEFAULT_NODE_NAME : sig->receiver_name; - dbc_string += " SG_ " + sig->name + " " + multiplexer_indicator + ": " + - std::to_string(sig->start_bit) + "|" + std::to_string(sig->size) + "@" + - std::string(1, sig->is_little_endian ? '1' : '0') + - std::string(1, sig->is_signed ? '-' : '+') + - " (" + doubleToString(sig->factor) + "," + doubleToString(sig->offset) + ")" + - " [" + doubleToString(sig->min) + "|" + doubleToString(sig->max) + "]" + - " \"" + sig->unit + "\" " + recv + "\n"; - if (!sig->comment.empty()) { - std::string escaped_comment = sig->comment; - for (size_t pos = 0; (pos = escaped_comment.find('"', pos)) != std::string::npos; pos += 2) - escaped_comment.replace(pos, 1, "\\\""); - comment += "CM_ SG_ " + std::to_string(address) + " " + sig->name + " \"" + escaped_comment + "\";\n"; - } - if (!sig->val_desc.empty()) { - std::string text; - for (auto &[val, desc] : sig->val_desc) { - if (!text.empty()) text += " "; - char val_buf[64]; - snprintf(val_buf, sizeof(val_buf), "%g", val); - text += std::string(val_buf) + " \"" + desc + "\""; - } - val_desc += "VAL_ " + std::to_string(address) + " " + sig->name + " " + text + ";\n"; - } - } - dbc_string += "\n"; - } - return header + dbc_string + comment + val_desc; -} diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc deleted file mode 100644 index e7b1129a30..0000000000 --- a/tools/cabana/settings.cc +++ /dev/null @@ -1,141 +0,0 @@ -#include "tools/cabana/settings.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/utils/util.h" - -const int MIN_CACHE_MINIUTES = 30; -const int MAX_CACHE_MINIUTES = 120; - -Settings settings; - -template -void settings_op(SettingOperation op) { - QSettings s("cabana"); - op(s, "absolute_time", settings.absolute_time); - op(s, "fps", settings.fps); - op(s, "max_cached_minutes", settings.max_cached_minutes); - op(s, "chart_height", settings.chart_height); - op(s, "chart_range", settings.chart_range); - op(s, "chart_column_count", settings.chart_column_count); - op(s, "last_dir", settings.last_dir); - op(s, "last_route_dir", settings.last_route_dir); - op(s, "window_state", settings.window_state); - op(s, "geometry", settings.geometry); - op(s, "video_splitter_state", settings.video_splitter_state); - op(s, "recent_files", settings.recent_files); - op(s, "message_header_state", settings.message_header_state); - op(s, "chart_series_type", settings.chart_series_type); - op(s, "theme", settings.theme); - op(s, "sparkline_range", settings.sparkline_range); - op(s, "multiple_lines_hex", settings.multiple_lines_hex); - op(s, "log_livestream", settings.log_livestream); - op(s, "log_path", settings.log_path); - op(s, "drag_direction", (int &)settings.drag_direction); - op(s, "suppress_defined_signals", settings.suppress_defined_signals); - op(s, "recent_dbc_file", settings.recent_dbc_file); - op(s, "active_msg_id", settings.active_msg_id); - op(s, "selected_msg_ids", settings.selected_msg_ids); - op(s, "active_charts", settings.active_charts); -} - -Settings::Settings() { - last_dir = last_route_dir = QDir::homePath(); - log_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/"; - settings_op([](QSettings &s, const QString &key, auto &value) { - if (auto v = s.value(key); v.canConvert>()) - value = v.value>(); - }); -} - -Settings::~Settings() { - settings_op([](QSettings &s, const QString &key, auto &v) { s.setValue(key, v); }); -} - -// SettingsDlg - -SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { - setWindowTitle(tr("Settings")); - QVBoxLayout *main_layout = new QVBoxLayout(this); - QGroupBox *groupbox = new QGroupBox("General"); - QFormLayout *form_layout = new QFormLayout(groupbox); - - form_layout->addRow(tr("Color Theme"), theme = new QComboBox(this)); - theme->setToolTip(tr("You may need to restart cabana after changes theme")); - theme->addItems({tr("Automatic"), tr("Light"), tr("Dark")}); - theme->setCurrentIndex(settings.theme); - - form_layout->addRow("FPS", fps = new QSpinBox(this)); - fps->setRange(10, 100); - fps->setSingleStep(10); - fps->setValue(settings.fps); - - form_layout->addRow(tr("Max Cached Minutes"), cached_minutes = new QSpinBox(this)); - cached_minutes->setRange(MIN_CACHE_MINIUTES, MAX_CACHE_MINIUTES); - cached_minutes->setSingleStep(1); - cached_minutes->setValue(settings.max_cached_minutes); - main_layout->addWidget(groupbox); - - groupbox = new QGroupBox("New Signal Settings"); - form_layout = new QFormLayout(groupbox); - form_layout->addRow(tr("Drag Direction"), drag_direction = new QComboBox(this)); - drag_direction->addItems({tr("MSB First"), tr("LSB First"), tr("Always Little Endian"), tr("Always Big Endian")}); - drag_direction->setCurrentIndex(settings.drag_direction); - main_layout->addWidget(groupbox); - - groupbox = new QGroupBox("Chart"); - form_layout = new QFormLayout(groupbox); - form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this)); - chart_height->setRange(100, 500); - chart_height->setSingleStep(10); - chart_height->setValue(settings.chart_height); - main_layout->addWidget(groupbox); - - log_livestream = new QGroupBox(tr("Enable live stream logging"), this); - log_livestream->setCheckable(true); - QHBoxLayout *path_layout = new QHBoxLayout(log_livestream); - path_layout->addWidget(log_path = new QLineEdit(settings.log_path, this)); - log_path->setReadOnly(true); - auto browse_btn = new QPushButton(tr("B&rowse...")); - path_layout->addWidget(browse_btn); - main_layout->addWidget(log_livestream); - - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - main_layout->addWidget(buttonBox); - setFixedSize(400, sizeHint().height()); - - QObject::connect(browse_btn, &QPushButton::clicked, [this]() { - QString fn = QFileDialog::getExistingDirectory( - this, tr("Log File Location"), - QStandardPaths::writableLocation(QStandardPaths::HomeLocation), - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); - if (!fn.isEmpty()) { - log_path->setText(fn); - } - }); - QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDlg::save); -} - -void SettingsDlg::save() { - if (std::exchange(settings.theme, theme->currentIndex()) != settings.theme) { - // set theme before emit changed - utils::setTheme(settings.theme); - } - settings.fps = fps->value(); - settings.max_cached_minutes = cached_minutes->value(); - settings.chart_height = chart_height->value(); - settings.log_livestream = log_livestream->isChecked(); - settings.log_path = log_path->text(); - settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex(); - emit settings.changed(); - QDialog::accept(); -} diff --git a/tools/cabana/settings.h b/tools/cabana/settings.h deleted file mode 100644 index 7ab50d1494..0000000000 --- a/tools/cabana/settings.h +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#define LIGHT_THEME 1 -#define DARK_THEME 2 - -class Settings : public QObject { - Q_OBJECT - -public: - enum DragDirection { - MsbFirst, - LsbFirst, - AlwaysLE, - AlwaysBE, - }; - - Settings(); - ~Settings(); - - bool absolute_time = false; - int fps = 10; - int max_cached_minutes = 30; - int chart_height = 200; - int chart_column_count = 1; - int chart_range = 3 * 60; // 3 minutes - int chart_series_type = 0; - int theme = 0; - int sparkline_range = 15; // 15 seconds - bool multiple_lines_hex = false; - bool log_livestream = true; - bool suppress_defined_signals = false; - QString log_path; - QString last_dir; - QString last_route_dir; - QByteArray geometry; - QByteArray video_splitter_state; - QByteArray window_state; - QStringList recent_files; - QByteArray message_header_state; - DragDirection drag_direction = MsbFirst; - - // session data - QString recent_dbc_file; - QString active_msg_id; - QStringList selected_msg_ids; - QStringList active_charts; - -signals: - void changed(); -}; - -class SettingsDlg : public QDialog { -public: - SettingsDlg(QWidget *parent); - void save(); - QSpinBox *fps; - QSpinBox *cached_minutes; - QSpinBox *chart_height; - QComboBox *chart_series_type; - QComboBox *theme; - QGroupBox *log_livestream; - QLineEdit *log_path; - QComboBox *drag_direction; -}; - -extern Settings settings; diff --git a/tools/cabana/streams/devicestream.cc b/tools/cabana/streams/devicestream.cc deleted file mode 100644 index 20eaa70cd6..0000000000 --- a/tools/cabana/streams/devicestream.cc +++ /dev/null @@ -1,94 +0,0 @@ -#include "tools/cabana/streams/devicestream.h" - -#include -#include - -#include "cereal/services.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -// DeviceStream - -DeviceStream::DeviceStream(QObject *parent, QString address) : zmq_address(address), LiveStream(parent) { -} - -DeviceStream::~DeviceStream() { - if (!bridge_process) - return; - - bridge_process->terminate(); - if (!bridge_process->waitForFinished(3000)) { - bridge_process->kill(); - bridge_process->waitForFinished(); - } -} - -void DeviceStream::start() { - if (!zmq_address.isEmpty()) { - bridge_process = new QProcess(this); - QString bridge_path = QCoreApplication::applicationDirPath() + "/../../cereal/messaging/bridge"; - bridge_process->start(QFileInfo(bridge_path).absoluteFilePath(), QStringList { zmq_address, "/\"can/\"" }); - - if (!bridge_process->waitForStarted()) { - QMessageBox::warning(nullptr, tr("Error"), tr("Failed to start bridge: %1").arg(bridge_process->errorString())); - return; - } - } - - LiveStream::start(); -} - -void DeviceStream::streamThread() { - zmq_address.isEmpty() ? unsetenv("ZMQ") : setenv("ZMQ", "1", 1); - - std::unique_ptr context(Context::create()); - std::unique_ptr sock(SubSocket::create(context.get(), "can", "127.0.0.1", false, true, services.at("can").queue_size)); - assert(sock != NULL); - // run as fast as messages come in - while (!QThread::currentThread()->isInterruptionRequested()) { - std::unique_ptr msg(sock->receive(true)); - if (!msg) { - QThread::msleep(50); - continue; - } - handleEvent(kj::ArrayPtr((capnp::word*)msg->getData(), msg->getSize() / sizeof(capnp::word))); - } -} - -// OpenDeviceWidget - -OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { - QRadioButton *msgq = new QRadioButton(tr("MSGQ")); - QRadioButton *zmq = new QRadioButton(tr("ZMQ")); - ip_address = new QLineEdit(this); - ip_address->setPlaceholderText(tr("Enter device Ip Address")); - QString ip_range = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])"; - QString pattern("^" + ip_range + "\\." + ip_range + "\\." + ip_range + "\\." + ip_range + "$"); - QRegularExpression re(pattern); - ip_address->setValidator(new QRegularExpressionValidator(re, this)); - - group = new QButtonGroup(this); - group->addButton(msgq, 0); - group->addButton(zmq, 1); - - QFormLayout *form_layout = new QFormLayout(this); - form_layout->addRow(msgq); - form_layout->addRow(zmq, ip_address); - QObject::connect(group, qOverload(&QButtonGroup::buttonToggled), [=](QAbstractButton *button, bool checked) { - ip_address->setEnabled(button == zmq && checked); - }); - zmq->setChecked(true); -} - -AbstractStream *OpenDeviceWidget::open() { - QString ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text(); - bool msgq = group->checkedId() == 0; - return new DeviceStream(qApp, msgq ? "" : ip); -} diff --git a/tools/cabana/utils/export.cc b/tools/cabana/utils/export.cc deleted file mode 100644 index a7f910193f..0000000000 --- a/tools/cabana/utils/export.cc +++ /dev/null @@ -1,45 +0,0 @@ -#include "tools/cabana/utils/export.h" - -#include -#include - -#include "tools/cabana/streams/abstractstream.h" - -namespace utils { - -void exportToCSV(const QString &file_name, std::optional msg_id) { - QFile file(file_name); - if (file.open(QIODevice::ReadWrite | QIODevice::Truncate)) { - QTextStream stream(&file); - stream << "time,addr,bus,data\n"; - for (auto e : msg_id ? can->events(*msg_id) : can->allEvents()) { - stream << QString::number(can->toSeconds(e->mono_time), 'f', 3) << "," - << "0x" << QString::number(e->address, 16) << "," << e->src << "," - << "0x" << QByteArray::fromRawData((const char *)e->dat, e->size).toHex().toUpper() << "\n"; - } - } -} - -void exportSignalsToCSV(const QString &file_name, const MessageId &msg_id) { - QFile file(file_name); - if (auto msg = dbc()->msg(msg_id); msg && msg->sigs.size() && file.open(QIODevice::ReadWrite | QIODevice::Truncate)) { - QTextStream stream(&file); - stream << "time,addr,bus"; - for (auto s : msg->sigs) - stream << "," << s->name.c_str(); - stream << "\n"; - - for (auto e : can->events(msg_id)) { - stream << QString::number(can->toSeconds(e->mono_time), 'f', 3) << "," - << "0x" << QString::number(e->address, 16) << "," << e->src; - for (auto s : msg->sigs) { - double value = 0; - s->getValue(e->dat, e->size, &value); - stream << "," << QString::number(value, 'f', s->precision); - } - stream << "\n"; - } - } -} - -} // namespace utils diff --git a/tools/cabana/utils/export.h b/tools/cabana/utils/export.h deleted file mode 100644 index 270906b163..0000000000 --- a/tools/cabana/utils/export.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include - -#include "tools/cabana/dbc/dbcmanager.h" - -namespace utils { -void exportToCSV(const QString &file_name, std::optional msg_id = std::nullopt); -void exportSignalsToCSV(const QString &file_name, const MessageId &msg_id); -} // namespace utils diff --git a/tools/car_porting/README.md b/tools/car_porting/README.md index 77492035ca..07ca2d0eba 100644 --- a/tools/car_porting/README.md +++ b/tools/car_porting/README.md @@ -6,13 +6,13 @@ Check out [this blog post](https://blog.comma.ai/how-to-write-a-car-port-for-ope Testing car ports in your car is very time-consuming. Check out these utilities to do basic checks on your work before running it in your car. -### [Cabana](/tools/cabana/README.md) +### [Cabana](/openpilot/tools/cabana/README.md) View your car's CAN signals through DBC files, which openpilot uses to parse and create messages that talk to the car. Example: ```bash -> tools/cabana/cabana '1bbe6bf2d62f58a8|2022-07-14--17-11-43' +> openpilot/tools/cabana/cabana '1bbe6bf2d62f58a8|2022-07-14--17-11-43' ``` ### [tools/car_porting/auto_fingerprint.py](/tools/car_porting/auto_fingerprint.py) @@ -25,17 +25,17 @@ Example: Attempting to add fw version for: OUTBACK ``` -### [selfdrive/car/tests/test_car_interfaces.py](/selfdrive/car/tests/test_car_interfaces.py) +### [openpilot/selfdrive/car/tests/test_car_interfaces.py](/openpilot/selfdrive/car/tests/test_car_interfaces.py) Finds common bugs for car interfaces, without even requiring a route. #### Example: Typo in signal name ```bash -> pytest selfdrive/car/tests/test_car_interfaces.py -k subaru # replace with the brand you are working on +> pytest openpilot/selfdrive/car/tests/test_car_interfaces.py -k subaru # replace with the brand you are working on ===================================================================== -FAILED selfdrive/car/tests/test_car_interfaces.py::TestCarInterfaces::test_car_interfaces_165_SUBARU_LEGACY_7TH_GEN - KeyError: 'CruiseControlOOPS' +FAILED openpilot/selfdrive/car/tests/test_car_interfaces.py::TestCarInterfaces::test_car_interfaces_165_SUBARU_LEGACY_7TH_GEN - KeyError: 'CruiseControlOOPS' ``` diff --git a/tools/car_porting/examples/find_segments_with_message.ipynb b/tools/car_porting/examples/find_segments_with_message.ipynb index af17bde52b..f91827f7e5 100644 --- a/tools/car_porting/examples/find_segments_with_message.ipynb +++ b/tools/car_porting/examples/find_segments_with_message.ipynb @@ -9,7 +9,6 @@ "source": [ "# Import all cars from opendbc\n", "\n", - "from opendbc.car import structs\n", "from opendbc.car.values import PLATFORMS as TEST_PLATFORMS\n", "\n", "# Example: add additional platforms/segments to test outside of commaCarSegments\n", @@ -147,8 +146,8 @@ } ], "source": [ - "from openpilot.tools.lib.logreader import LogReader, comma_car_segments_source\n", - "from tqdm.notebook import tqdm, tnrange\n", + "from openpilot.tools.lib.logreader import comma_car_segments_source\n", + "from tqdm.notebook import tnrange\n", "\n", "# Example search for CAN ignition messages\n", "# Be careful when filtering by bus, account for odd harness arrangements on Honda/HKG\n", diff --git a/tools/car_porting/examples/ford_vin_fingerprint.ipynb b/tools/car_porting/examples/ford_vin_fingerprint.ipynb index 6b806d22d2..cb5fd8f820 100644 --- a/tools/car_porting/examples/ford_vin_fingerprint.ipynb +++ b/tools/car_porting/examples/ford_vin_fingerprint.ipynb @@ -53,12 +53,12 @@ " if vin.startswith('1FT'):\n", " if vin_positions_567 in F150_CODES:\n", " if vin[7] in LIGHTNING_CODES:\n", - " return f\"FORD F-150 LIGHTNING 1ST GEN\"\n", + " return \"FORD F-150 LIGHTNING 1ST GEN\"\n", " else:\n", - " return f\"FORD F-150 14TH GEN\"\n", + " return \"FORD F-150 14TH GEN\"\n", " elif vin.startswith('3FM'):\n", " if vin_positions_567 in MACHE_CODES:\n", - " return f\"FORD MUSTANG MACH-E 1ST GEN\"\n", + " return \"FORD MUSTANG MACH-E 1ST GEN\"\n", " elif vin.startswith('5LM'):\n", " pass\n", "\n", @@ -147,7 +147,8 @@ "source": [ "for vin, real_fingerprint in VINS_TO_CHECK:\n", " determined_fingerprint = ford_vin_fingerprint(vin)\n", - " print(f\"vin: {vin} real platform: {real_fingerprint: <30} determined platform: {determined_fingerprint: <30} correct: {real_fingerprint == determined_fingerprint}\")" + " print(f\"vin: {vin} real platform: {real_fingerprint: <30} \" +\n", + " f\"determined platform: {determined_fingerprint: <30} correct: {real_fingerprint == determined_fingerprint}\")" ] } ], diff --git a/tools/car_porting/examples/hkg_canfd_gear_message.ipynb b/tools/car_porting/examples/hkg_canfd_gear_message.ipynb index f0bca8decc..ec902b7d10 100644 --- a/tools/car_porting/examples/hkg_canfd_gear_message.ipynb +++ b/tools/car_porting/examples/hkg_canfd_gear_message.ipynb @@ -21,9 +21,7 @@ } ], "source": [ - "from opendbc.car import structs\n", "from opendbc.car.hyundai.values import CAR, HyundaiFlags\n", - "from opendbc.car.hyundai.fingerprints import FW_VERSIONS\n", "\n", "TEST_PLATFORMS = set(CAR.with_flags(HyundaiFlags.CANFD)) & set(CAR.with_flags(HyundaiFlags.EV)) # CAN-FD electric vehicles only\n", "#TEST_PLATFORMS = set(CAR.with_flags(HyundaiFlags.CANFD)) - set(CAR.with_flags(HyundaiFlags.EV)) # CAN-FD hybrid and ICE vehicles only\n", @@ -190,15 +188,13 @@ ], "source": [ "import copy\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", "\n", "from opendbc.can.parser import CANParser\n", "from opendbc.car.hyundai.values import DBC\n", "from opendbc.car.hyundai.hyundaicanfd import CanBus\n", "\n", "from openpilot.selfdrive.pandad import can_capnp_to_list\n", - "from openpilot.tools.lib.logreader import LogReader, comma_car_segments_source\n", + "from openpilot.tools.lib.logreader import comma_car_segments_source\n", "\n", "message_names = [\"GEAR_SHIFTER\", \"ACCELERATOR\", \"GEAR\", \"GEAR_ALT\", \"GEAR_ALT_2\"]\n", "\n", @@ -229,11 +225,11 @@ " for i, parsed_messages in enumerate(parsed_message_history):\n", " gear = parsed_messages[name][\"GEAR\"]\n", " if gear != gear_prev:\n", - " print(f\" *** Signal transition found! ***\")\n", + " print(\" *** Signal transition found! ***\")\n", " examples.append(i)\n", " gear_prev = gear\n", "\n", - "print(f\"Analysis finished\")\n" + "print(\"Analysis finished\")\n" ] }, { diff --git a/tools/car_porting/measure_steering_accuracy.py b/tools/car_porting/measure_steering_accuracy.py index ae3344c2eb..fa1da959a7 100755 --- a/tools/car_porting/measure_steering_accuracy.py +++ b/tools/car_porting/measure_steering_accuracy.py @@ -6,7 +6,7 @@ import argparse import signal from collections import defaultdict -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.tools.lib.logreader import LogReader def sigint_handler(signal, frame): diff --git a/tools/car_porting/test_car_model.py b/tools/car_porting/test_car_model.py index dd248f562e..61ec2e15ed 100755 --- a/tools/car_porting/test_car_model.py +++ b/tools/car_porting/test_car_model.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 import argparse import sys -import unittest # noqa: TID251 - +import unittest from opendbc.car.tests.routes import CarTestRoute from openpilot.selfdrive.car.tests.test_models import TestCarModel from openpilot.tools.lib.route import SegmentRange @@ -20,7 +19,7 @@ def create_test_models_suite(routes: list[CarTestRoute]) -> unittest.TestSuite: if __name__ == "__main__": parser = argparse.ArgumentParser(description="Test any route against common issues with a new car port. " + - "Uses selfdrive/car/tests/test_models.py") + "Uses openpilot/selfdrive/car/tests/test_models.py") parser.add_argument("route_or_segment_name", help="Specify route to run tests on") parser.add_argument("--car", help="Specify car model for test route") args = parser.parse_args() diff --git a/tools/op.sh b/tools/op.sh index 538d689569..1ee7b232b0 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -48,7 +48,7 @@ function retry() { } function op_run_command() { - CMD="$@" + CMD="$*" echo -e "${BOLD}Running command →${NC} $CMD │" for ((i=0; i<$((19 + ${#CMD})); i++)); do @@ -57,7 +57,7 @@ function op_run_command() { echo -e "┘\n" if [[ -z "$DRY" ]]; then - eval "$CMD" + "$@" fi } @@ -75,7 +75,7 @@ function op_get_openpilot_dir() { # Fallback to hardcoded directories if not found SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" - for dir in "${SCRIPT_DIR%/tools}" "$HOME/openpilot" "/data/openpilot"; do + for dir in "$(readlink -f "$SCRIPT_DIR/../..")" "$HOME/openpilot" "/data/openpilot"; do if [[ -f "$dir/launch_openpilot.sh" ]]; then OPENPILOT_ROOT="$dir" return 0 @@ -116,7 +116,7 @@ function op_check_git() { fi echo "Checking for git lfs files..." - if [[ $(file -b $OPENPILOT_ROOT/selfdrive/modeld/models/dmonitoring_model.onnx) == "data" ]]; then + if [[ $(file -b $OPENPILOT_ROOT/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx) == "data" ]]; then echo -e " ↳ [${GREEN}✔${NC}] git lfs files found." else echo -e " ↳ [${RED}✗${NC}] git lfs files not found! Run 'git lfs pull'" @@ -199,6 +199,17 @@ function op_setup() { op_check_openpilot_dir op_check_os + # Submodules must be present before uv sync: pyproject path sources + # (pandacan, opendbc, msgq, ...) live in the submodule checkouts. + echo "Getting git submodules..." + st="$(date +%s)" + if ! retry 3 git submodule update --jobs 4 --init --recursive; then + echo -e " ↳ [${RED}✗${NC}] Getting git submodules failed!" + return 1 + fi + et="$(date +%s)" + echo -e " ↳ [${GREEN}✔${NC}] Submodules installed successfully in $((et - st)) seconds." + echo "Installing dependencies..." st="$(date +%s)" SETUP_SCRIPT="tools/setup_dependencies.sh" @@ -211,15 +222,6 @@ function op_setup() { op_activate_venv - echo "Getting git submodules..." - st="$(date +%s)" - if ! retry 3 git submodule update --jobs 4 --init --recursive; then - echo -e " ↳ [${RED}✗${NC}] Getting git submodules failed!" - return 1 - fi - et="$(date +%s)" - echo -e " ↳ [${GREEN}✔${NC}] Submodules installed successfully in $((et - st)) seconds." - echo "Pulling git lfs files..." st="$(date +%s)" if ! retry 3 git lfs pull; then @@ -234,7 +236,7 @@ function op_setup() { function op_auth() { op_before_cmd - op_run_command tools/lib/auth.py "$@" + op_run_command openpilot/tools/lib/auth.py "$@" } function op_activate_venv() { @@ -298,7 +300,7 @@ function op_check() { function op_esim() { op_before_cmd - op_run_command system/hardware/esim.py "$@" + op_run_command openpilot/common/esim/esim.py "$@" } function op_build() { @@ -307,47 +309,46 @@ function op_build() { cd "$CDIR" if [[ -f "/AGNOS" ]]; then # needed on AGNOS to not run out of memory - op_run_command system/manager/build.py + op_run_command openpilot/system/manager/build.py else - # scons is fine on PC - op_run_command scons $@ + op_run_command scons -u "$@" fi } function op_juggle() { op_before_cmd - op_run_command tools/plotjuggler/juggle.py $@ + op_run_command openpilot/tools/plotjuggler/juggle.py "$@" } function op_lint() { op_before_cmd - op_run_command scripts/lint/lint.sh $@ + op_run_command scripts/lint/lint.sh "$@" } function op_test() { op_before_cmd - op_run_command pytest $@ + op_run_command pytest "$@" } function op_replay() { op_before_cmd - op_run_command tools/replay/replay $@ + op_run_command openpilot/tools/replay/replay "$@" } function op_cabana() { op_before_cmd - op_run_command tools/cabana/cabana $@ + op_run_command openpilot/tools/cabana/cabana "$@" } function op_sim() { op_before_cmd - op_run_command exec tools/sim/run_bridge.py & - op_run_command exec tools/sim/launch_openpilot.sh + op_run_command exec openpilot/tools/sim/run_bridge.py & + op_run_command exec openpilot/tools/sim/launch_openpilot.sh } function op_clip() { op_before_cmd - op_run_command tools/clip/run.py $@ + op_run_command openpilot/tools/clip/run.py "$@" } function op_switch() { @@ -489,4 +490,4 @@ function _op() { esac } -_op $@ +_op "$@" diff --git a/release/README.md b/tools/release/README.md similarity index 82% rename from release/README.md rename to tools/release/README.md index 7aeea9fe4a..862836bf90 100644 --- a/release/README.md +++ b/tools/release/README.md @@ -11,8 +11,8 @@ - [ ] push the new branch - [ ] push to staging: - [ ] make sure you are on the newly created release master branch (`zerotentwo`) - - [ ] run `BRANCH=devel-staging release/build_stripped.sh`. Jenkins will then automatically build staging on device, run `test_onroad` and update the staging branch -- [ ] bump version on master: `common/version.h` and `RELEASES.md` + - [ ] run `BRANCH=devel-staging tools/release/build_stripped.sh`. Jenkins will then automatically build staging on device, run `test_onroad` and update the staging branch +- [ ] bump version on master: `openpilot/common/version.h` and `RELEASES.md` - [ ] post on Discord, tag `@release crew` ### Go to release diff --git a/release/build_release.sh b/tools/release/build_release.sh similarity index 76% rename from release/build_release.sh rename to tools/release/build_release.sh index c038e0e78e..6a019a80d0 100755 --- a/release/build_release.sh +++ b/tools/release/build_release.sh @@ -33,7 +33,7 @@ git checkout --orphan $BUILD_BRANCH # do the files copy echo "[-] copying files T=$SECONDS" cd $SOURCE_DIR -cp -pR --parents $(./release/release_files.py) $BUILD_DIR/ +cp -pR --parents $(./tools/release/release_files.py) $BUILD_DIR/ # in the directory cd $BUILD_DIR @@ -41,7 +41,7 @@ cd $BUILD_DIR rm -f panda/board/obj/panda.bin.signed rm -f panda/board/obj/panda_h7.bin.signed -VERSION=$(cat sunnypilot/common/version.h | awk -F[\"-] '{print $2}') +VERSION=$(cat openpilot/sunnypilot/common/version.h | awk -F[\"-] '{print $2}') echo "[-] committing version $VERSION T=$SECONDS" git add -f . git commit -a -m "openpilot v$VERSION release" @@ -73,16 +73,16 @@ find . -name '*.os' -delete find . -name '*.pyc' -delete find . -name 'moc_*' -delete find . -name '__pycache__' -delete -rm -rf .sconsign.dblite Jenkinsfile release/ -rm -f selfdrive/modeld/models/*.onnx* -rm -f sunnypilot/modeld*/models/*.onnx* +rm -rf .sconsign.dblite Jenkinsfile tools/release/ +rm -f openpilot/selfdrive/modeld/models/*.onnx* +rm -f openpilot/sunnypilot/modeld*/models/*.onnx* -find third_party/ -name '*x86*' -exec rm -r {} + -find third_party/ -name '*Darwin*' -exec rm -r {} + +find openpilot/third_party/ -name '*x86*' -exec rm -r {} + +find openpilot/third_party/ -name '*Darwin*' -exec rm -r {} + # Restore third_party -git checkout third_party/ +git checkout openpilot/third_party/ # Mark as prebuilt release touch prebuilt @@ -93,8 +93,8 @@ git commit --amend -m "openpilot v$VERSION" # Run tests cd $BUILD_DIR -RELEASE=1 pytest -n0 -s selfdrive/test/test_onroad.py -#pytest selfdrive/car/tests/test_car_interfaces.py +RELEASE=1 pytest -n0 -s openpilot/selfdrive/test/test_onroad.py +#pytest openpilot/selfdrive/car/tests/test_car_interfaces.py echo "[-] pushing release T=$SECONDS" REFS=() diff --git a/release/build_stripped.sh b/tools/release/build_stripped.sh similarity index 87% rename from release/build_stripped.sh rename to tools/release/build_stripped.sh index 8e73a8aaab..b5092b15a7 100755 --- a/release/build_stripped.sh +++ b/tools/release/build_stripped.sh @@ -38,20 +38,20 @@ git submodule foreach --recursive git clean -xdff # do the files copy echo "[-] copying files T=$SECONDS" cd $SOURCE_DIR -cp -pR --parents $(./release/release_files.py) $TARGET_DIR/ +./tools/release/release_files.py | xargs -d '\n' cp -pR --parents -t "$TARGET_DIR" # in the directory cd $TARGET_DIR rm -rf .git/modules/ rm -f panda/board/obj/panda.bin.signed -find selfdrive/modeld/models -name '*.onnx' -size +95M -exec ./common/file_chunker.py {} \; +find openpilot/selfdrive/modeld/models -name '*.onnx' -size +95M -exec ./openpilot/common/file_chunker.py {} \; # include source commit hash and build date in commit GIT_HASH=$(git --git-dir=$SOURCE_DIR/.git rev-parse HEAD) GIT_COMMIT_DATE=$(git --git-dir=$SOURCE_DIR/.git show --no-patch --format='%ct %ci' HEAD) DATETIME=$(date '+%Y-%m-%dT%H:%M:%S') -VERSION=$(cat $SOURCE_DIR/sunnypilot/common/version.h | awk -F\" '{print $2}') +VERSION=$(cat $SOURCE_DIR/openpilot/sunnypilot/common/version.h | awk -F\" '{print $2}') echo -n "$GIT_HASH" > git_src_commit echo -n "$GIT_COMMIT_DATE" > git_src_commit_date diff --git a/release/check-dirty.sh b/tools/release/check-dirty.sh similarity index 100% rename from release/check-dirty.sh rename to tools/release/check-dirty.sh diff --git a/release/check-submodules.sh b/tools/release/check-submodules.sh similarity index 100% rename from release/check-submodules.sh rename to tools/release/check-submodules.sh diff --git a/release/identity.sh b/tools/release/identity.sh similarity index 100% rename from release/identity.sh rename to tools/release/identity.sh diff --git a/release/pack.py b/tools/release/pack.py similarity index 96% rename from release/pack.py rename to tools/release/pack.py index 560500e7a7..f29ea034db 100755 --- a/release/pack.py +++ b/tools/release/pack.py @@ -11,9 +11,9 @@ from pathlib import Path from openpilot.common.basedir import BASEDIR -DIRS = ['cereal', 'openpilot'] +DIRS = ['openpilot'] EXTS = ['.png', '.py', '.ttf', '.capnp', '.json', '.fnt', '.mo', '.po'] -EXCLUDE = ['selfdrive/assets/training'] +EXCLUDE = ['openpilot/selfdrive/assets/training'] INTERPRETER = '/usr/bin/env python3' diff --git a/release/release_files.py b/tools/release/release_files.py similarity index 93% rename from release/release_files.py rename to tools/release/release_files.py index 0b9e034bf1..7f08719ea8 100755 --- a/release/release_files.py +++ b/tools/release/release_files.py @@ -4,7 +4,7 @@ import re from pathlib import Path HERE = os.path.abspath(os.path.dirname(__file__)) -ROOT = HERE + "/.." +ROOT = os.path.abspath(os.path.join(HERE, "../..")) blacklist = [ ".git/", diff --git a/tools/sim/__init__.py b/tools/scripts/__init__.py similarity index 100% rename from tools/sim/__init__.py rename to tools/scripts/__init__.py diff --git a/tools/scripts/adb_ssh.sh b/tools/scripts/adb_ssh.sh index b9668e7e0b..c584a72f2b 100755 --- a/tools/scripts/adb_ssh.sh +++ b/tools/scripts/adb_ssh.sh @@ -5,7 +5,7 @@ set -euo pipefail while IFS=' ' read -r name port; do adb forward "tcp:${port}" "tcp:${port}" > /dev/null done < <(python3 - <<'PY' -from cereal.services import SERVICE_LIST +from openpilot.cereal.services import SERVICE_LIST FNV_PRIME = 0x100000001b3 FNV_OFFSET_BASIS = 0xcbf29ce484222325 diff --git a/selfdrive/debug/can_print_changes.py b/tools/scripts/car/can_print_changes.py similarity index 97% rename from selfdrive/debug/can_print_changes.py rename to tools/scripts/car/can_print_changes.py index 97d60b2b05..e466cd2968 100755 --- a/selfdrive/debug/can_print_changes.py +++ b/tools/scripts/car/can_print_changes.py @@ -4,8 +4,8 @@ import binascii import time from collections import defaultdict -import cereal.messaging as messaging -from openpilot.selfdrive.debug.can_table import can_table +import openpilot.cereal.messaging as messaging +from tools.scripts.car.can_table import can_table from openpilot.tools.lib.logreader import LogIterable, LogReader RED = '\033[91m' diff --git a/selfdrive/debug/can_printer.py b/tools/scripts/car/can_printer.py similarity index 97% rename from selfdrive/debug/can_printer.py rename to tools/scripts/car/can_printer.py index f2ed6730d3..85f129f795 100755 --- a/selfdrive/debug/can_printer.py +++ b/tools/scripts/car/can_printer.py @@ -4,7 +4,7 @@ import binascii import time from collections import defaultdict -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging def can_printer(bus, max_msg, addr, ascii_decode): diff --git a/selfdrive/debug/can_table.py b/tools/scripts/car/can_table.py similarity index 96% rename from selfdrive/debug/can_table.py rename to tools/scripts/car/can_table.py index 11d070e708..e2d9874fec 100755 --- a/selfdrive/debug/can_table.py +++ b/tools/scripts/car/can_table.py @@ -2,7 +2,7 @@ import argparse import pandas as pd -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging def can_table(dat): diff --git a/selfdrive/debug/car/clear_dtc.py b/tools/scripts/car/clear_dtc.py similarity index 100% rename from selfdrive/debug/car/clear_dtc.py rename to tools/scripts/car/clear_dtc.py diff --git a/selfdrive/debug/car/disable_ecu.py b/tools/scripts/car/disable_ecu.py similarity index 91% rename from selfdrive/debug/car/disable_ecu.py rename to tools/scripts/car/disable_ecu.py index 14d0cbb9cf..884b86896b 100755 --- a/selfdrive/debug/car/disable_ecu.py +++ b/tools/scripts/car/disable_ecu.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import time -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from opendbc.car.disable_ecu import disable_ecu from openpilot.selfdrive.car.card import can_comm_callbacks diff --git a/selfdrive/debug/car/ecu_addrs.py b/tools/scripts/car/ecu_addrs.py similarity index 89% rename from selfdrive/debug/car/ecu_addrs.py rename to tools/scripts/car/ecu_addrs.py index f3f06406c0..0dfce880b0 100755 --- a/selfdrive/debug/car/ecu_addrs.py +++ b/tools/scripts/car/ecu_addrs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import argparse import time -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from opendbc.car.carlog import carlog from opendbc.car.ecu_addrs import get_all_ecu_addrs from openpilot.common.params import Params @@ -26,9 +26,9 @@ if __name__ == "__main__": # Set up params for pandad params = Params() params.remove("FirmwareQueryDone") - params.put_bool("IsOnroad", False, block=True) + params.put_bool("IsOffroad", True, block=True) time.sleep(0.2) # thread is 10 Hz - params.put_bool("IsOnroad", True, block=True) + params.put_bool("IsOffroad", False, block=True) obd_callback(params)(not args.no_obd) diff --git a/selfdrive/debug/car/fw_versions.py b/tools/scripts/car/fw_versions.py similarity index 92% rename from selfdrive/debug/car/fw_versions.py rename to tools/scripts/car/fw_versions.py index b04f9cd2a6..e68447d69e 100755 --- a/selfdrive/debug/car/fw_versions.py +++ b/tools/scripts/car/fw_versions.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 import time import argparse -import cereal.messaging as messaging -from cereal import car +import openpilot.cereal.messaging as messaging +from opendbc.car.structs import car from opendbc.car.carlog import carlog from opendbc.car.fw_versions import get_fw_versions, match_fw_to_car from opendbc.car.vin import get_vin @@ -30,9 +30,9 @@ if __name__ == "__main__": # Set up params for pandad params = Params() params.remove("FirmwareQueryDone") - params.put_bool("IsOnroad", False, block=True) + params.put_bool("IsOffroad", True, block=True) time.sleep(0.2) # thread is 10 Hz - params.put_bool("IsOnroad", True, block=True) + params.put_bool("IsOffroad", False, block=True) set_obd_multiplexing = obd_callback(params) extra: Any = None diff --git a/selfdrive/debug/car/hyundai_enable_radar_points.py b/tools/scripts/car/hyundai_enable_radar_points.py similarity index 99% rename from selfdrive/debug/car/hyundai_enable_radar_points.py rename to tools/scripts/car/hyundai_enable_radar_points.py index 1cab7b0270..5fbd13f2b0 100755 --- a/selfdrive/debug/car/hyundai_enable_radar_points.py +++ b/tools/scripts/car/hyundai_enable_radar_points.py @@ -15,7 +15,6 @@ import sys import argparse from typing import NamedTuple from subprocess import check_output, CalledProcessError - from opendbc.car.carlog import carlog from opendbc.car.uds import UdsClient, SESSION_TYPE, DATA_IDENTIFIER_TYPE from opendbc.car.structs import CarParams diff --git a/selfdrive/debug/max_lat_accel.py b/tools/scripts/car/max_lat_accel.py similarity index 100% rename from selfdrive/debug/max_lat_accel.py rename to tools/scripts/car/max_lat_accel.py diff --git a/selfdrive/debug/measure_torque_time_to_max.py b/tools/scripts/car/measure_torque_time_to_max.py similarity index 95% rename from selfdrive/debug/measure_torque_time_to_max.py rename to tools/scripts/car/measure_torque_time_to_max.py index e99aeae464..df41e5738e 100755 --- a/selfdrive/debug/measure_torque_time_to_max.py +++ b/tools/scripts/car/measure_torque_time_to_max.py @@ -6,8 +6,8 @@ import struct from collections import deque from statistics import mean -from cereal import log -import cereal.messaging as messaging +from openpilot.cereal import log +import openpilot.cereal.messaging as messaging if __name__ == "__main__": diff --git a/selfdrive/debug/read_dtc_status.py b/tools/scripts/car/read_dtc_status.py similarity index 100% rename from selfdrive/debug/read_dtc_status.py rename to tools/scripts/car/read_dtc_status.py diff --git a/selfdrive/debug/car/toyota_eps_factor.py b/tools/scripts/car/toyota_eps_factor.py similarity index 100% rename from selfdrive/debug/car/toyota_eps_factor.py rename to tools/scripts/car/toyota_eps_factor.py diff --git a/selfdrive/debug/car/vin.py b/tools/scripts/car/vin.py similarity index 94% rename from selfdrive/debug/car/vin.py rename to tools/scripts/car/vin.py index 9b1d6528cc..732d3d3f4a 100755 --- a/selfdrive/debug/car/vin.py +++ b/tools/scripts/car/vin.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import argparse import time -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from opendbc.car.carlog import carlog from opendbc.car.vin import get_vin from openpilot.selfdrive.car.card import can_comm_callbacks diff --git a/selfdrive/debug/car/vw_mqb_config.py b/tools/scripts/car/vw_mqb_config.py similarity index 100% rename from selfdrive/debug/car/vw_mqb_config.py rename to tools/scripts/car/vw_mqb_config.py diff --git a/selfdrive/debug/count_events.py b/tools/scripts/count_events.py similarity index 97% rename from selfdrive/debug/count_events.py rename to tools/scripts/count_events.py index 76e02d414e..a7e71210ac 100755 --- a/selfdrive/debug/count_events.py +++ b/tools/scripts/count_events.py @@ -6,7 +6,7 @@ from collections import Counter from pprint import pprint from typing import cast -from cereal.services import SERVICE_LIST +from openpilot.cereal.services import SERVICE_LIST from openpilot.tools.lib.logreader import LogReader, ReadMode from openpilot.selfdrive.test.process_replay.migration import migrate_all diff --git a/selfdrive/debug/cycle_alerts.py b/tools/scripts/cycle_alerts.py similarity index 97% rename from selfdrive/debug/cycle_alerts.py rename to tools/scripts/cycle_alerts.py index 4b1def4fc6..0bd42bcd3f 100755 --- a/selfdrive/debug/cycle_alerts.py +++ b/tools/scripts/cycle_alerts.py @@ -2,8 +2,9 @@ import time import random -from cereal import car, log -import cereal.messaging as messaging +from openpilot.cereal import log +from opendbc.car.structs import car +import openpilot.cereal.messaging as messaging from opendbc.car.honda.interface import CarInterface from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.selfdrived.events import ET, Events diff --git a/selfdrive/debug/debug_fw_fingerprinting_offline.py b/tools/scripts/debug_fw_fingerprinting_offline.py similarity index 99% rename from selfdrive/debug/debug_fw_fingerprinting_offline.py rename to tools/scripts/debug_fw_fingerprinting_offline.py index d36b350bbc..b33817e944 100755 --- a/selfdrive/debug/debug_fw_fingerprinting_offline.py +++ b/tools/scripts/debug_fw_fingerprinting_offline.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import argparse - from opendbc.car import uds from openpilot.tools.lib.live_logreader import live_logreader from openpilot.tools.lib.logreader import LogReader, ReadMode diff --git a/selfdrive/debug/dump.py b/tools/scripts/dump.py similarity index 93% rename from selfdrive/debug/dump.py rename to tools/scripts/dump.py index 78cf51e3db..e4f5495f9b 100755 --- a/selfdrive/debug/dump.py +++ b/tools/scripts/dump.py @@ -4,8 +4,8 @@ import argparse import json import codecs -from cereal import log -from cereal.services import SERVICE_LIST +from openpilot.cereal import log +from openpilot.cereal.services import SERVICE_LIST from openpilot.tools.lib.live_logreader import raw_live_logreader @@ -25,7 +25,7 @@ def hexdump(msg): if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Dump communication sockets. See cereal/services.py for a complete list of available sockets.') + parser = argparse.ArgumentParser(description='Dump communication sockets. See openpilot/cereal/services.py for a complete list of available sockets.') parser.add_argument('--pipe', action='store_true') parser.add_argument('--raw', action='store_true') parser.add_argument('--json', action='store_true') diff --git a/tools/scripts/fetch_image_from_route.py b/tools/scripts/fetch_image_from_route.py deleted file mode 100755 index 77bf48f946..0000000000 --- a/tools/scripts/fetch_image_from_route.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -import sys - -if len(sys.argv) < 4: - print(f"{sys.argv[0]} [front|wide|driver]") - print('example: ./fetch_image_from_route.py "02c45f73a2e5c6e9|2020-06-01--18-03-08" 3 500 driver') - exit(0) - -cameras = { - "front": "cameras", - "wide": "ecameras", - "driver": "dcameras" -} - -import requests -from PIL import Image -from openpilot.tools.lib.auth_config import get_token -from openpilot.tools.lib.framereader import FrameReader - -jwt = get_token() - -route = sys.argv[1] -segment = int(sys.argv[2]) -frame = int(sys.argv[3]) -camera = cameras[sys.argv[4]] if len(sys.argv) > 4 and sys.argv[4] in cameras else "cameras" - -url = f'https://api.commadotai.com/v1/route/{route}/files' -r = requests.get(url, headers={"Authorization": f"JWT {jwt}"}, timeout=10) -assert r.status_code == 200 -print("got api response") - -segments = r.json()[camera] -if segment >= len(segments): - raise Exception(f"segment {segment} not found, got {len(segments)} segments") - -fr = FrameReader(segments[segment]) -if frame >= fr.frame_count: - raise Exception("frame {frame} not found, got {fr.frame_count} frames") - -im = Image.fromarray(fr.get(frame)) -fn = f"uxxx_{route.replace('|', '_')}_{segment}_{frame}.png" -im.save(fn) -print(f"saved {fn}") diff --git a/selfdrive/debug/filter_log_message.py b/tools/scripts/filter_log_message.py similarity index 77% rename from selfdrive/debug/filter_log_message.py rename to tools/scripts/filter_log_message.py index 9cbab0b41f..1ec0333169 100755 --- a/selfdrive/debug/filter_log_message.py +++ b/tools/scripts/filter_log_message.py @@ -2,7 +2,7 @@ import argparse import json -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging from openpilot.tools.lib.logreader import LogReader LEVELS = { @@ -13,7 +13,7 @@ LEVELS = { "CRITICAL": 50, } -ANDROID_LOG_SOURCE = { +OPERATING_SYSTEM_LOG_SOURCE = { 0: "MAIN", 1: "RADIO", 2: "EVENTS", @@ -34,8 +34,8 @@ def print_logmessage(t, msg, min_level): print(f"[{t / 1e9:.6f}] decode error: {msg}") -def print_androidlog(t, msg): - source = ANDROID_LOG_SOURCE[msg.id] +def print_operating_system_log(t, msg): + source = msg.tag or OPERATING_SYSTEM_LOG_SOURCE.get(msg.id, "SYSTEM") try: m = json.loads(msg.message)['MESSAGE'] except Exception: @@ -65,15 +65,15 @@ if __name__ == "__main__": print_logmessage(m.logMonoTime-st, m.logMessage, min_level) elif m.which() == 'errorLogMessage': print_logmessage(m.logMonoTime-st, m.errorLogMessage, min_level) - elif m.which() == 'androidLog': - print_androidlog(m.logMonoTime-st, m.androidLog) + elif m.which() == 'operatingSystemLog': + print_operating_system_log(m.logMonoTime-st, m.operatingSystemLog) else: - sm = messaging.SubMaster(['logMessage', 'androidLog'], addr=args.addr) + sm = messaging.SubMaster(['logMessage', 'operatingSystemLog'], addr=args.addr) while True: sm.update() if sm.updated['logMessage']: print_logmessage(sm.logMonoTime['logMessage'], sm['logMessage'], min_level) - if sm.updated['androidLog']: - print_androidlog(sm.logMonoTime['androidLog'], sm['androidLog']) + if sm.updated['operatingSystemLog']: + print_operating_system_log(sm.logMonoTime['operatingSystemLog'], sm['operatingSystemLog']) diff --git a/selfdrive/debug/fingerprint_from_route.py b/tools/scripts/fingerprint_from_route.py similarity index 100% rename from selfdrive/debug/fingerprint_from_route.py rename to tools/scripts/fingerprint_from_route.py diff --git a/selfdrive/debug/fuzz_fw_fingerprint.py b/tools/scripts/fuzz_fw_fingerprint.py similarity index 99% rename from selfdrive/debug/fuzz_fw_fingerprint.py rename to tools/scripts/fuzz_fw_fingerprint.py index b4276c0c1a..5b0ba9cfd0 100755 --- a/selfdrive/debug/fuzz_fw_fingerprint.py +++ b/tools/scripts/fuzz_fw_fingerprint.py @@ -3,7 +3,6 @@ import random from collections import defaultdict from tqdm import tqdm - from opendbc.car.fw_versions import match_fw_to_car_fuzzy from opendbc.car.toyota.values import FW_VERSIONS as TOYOTA_FW_VERSIONS from opendbc.car.honda.values import FW_VERSIONS as HONDA_FW_VERSIONS diff --git a/selfdrive/debug/get_fingerprint.py b/tools/scripts/get_fingerprint.py similarity index 91% rename from selfdrive/debug/get_fingerprint.py rename to tools/scripts/get_fingerprint.py index 75c3d5579f..1e5e8eca5a 100755 --- a/selfdrive/debug/get_fingerprint.py +++ b/tools/scripts/get_fingerprint.py @@ -4,13 +4,13 @@ # Instructions: # - connect to a Panda -# - run selfdrive/pandad/pandad +# - run openpilot/selfdrive/pandad/pandad # - launching this script # Note: it's very important that the car is in stock mode, in order to collect a complete fingerprint # - since some messages are published at low frequency, keep this script running for at least 30s, # until all messages are received at least once -import cereal.messaging as messaging +import openpilot.cereal.messaging as messaging logcan = messaging.sub_sock('can') msgs = {} diff --git a/selfdrive/debug/live_cpu_and_temp.py b/tools/scripts/live_cpu_and_temp.py similarity index 98% rename from selfdrive/debug/live_cpu_and_temp.py rename to tools/scripts/live_cpu_and_temp.py index ee0524ec9d..484207d0fe 100755 --- a/selfdrive/debug/live_cpu_and_temp.py +++ b/tools/scripts/live_cpu_and_temp.py @@ -4,7 +4,7 @@ import numpy as np import capnp from collections import defaultdict -from cereal.messaging import SubMaster +from openpilot.cereal.messaging import SubMaster def cputime_total(ct): return ct.user + ct.nice + ct.system + ct.idle + ct.iowait + ct.irq + ct.softirq diff --git a/tools/scripts/mem_usage.py b/tools/scripts/mem_usage.py new file mode 100755 index 0000000000..9b13a56ce2 --- /dev/null +++ b/tools/scripts/mem_usage.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import argparse +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +from openpilot.selfdrive.test.mem_usage import DEMO_ROUTE, print_report +from openpilot.tools.lib.logreader import LogReader + + +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/debug/print_flags.py b/tools/scripts/print_flags.py similarity index 100% rename from selfdrive/debug/print_flags.py rename to tools/scripts/print_flags.py diff --git a/tools/profiling/clpeak/.gitignore b/tools/scripts/profiling/clpeak/.gitignore similarity index 100% rename from tools/profiling/clpeak/.gitignore rename to tools/scripts/profiling/clpeak/.gitignore diff --git a/tools/profiling/clpeak/build.sh b/tools/scripts/profiling/clpeak/build.sh similarity index 100% rename from tools/profiling/clpeak/build.sh rename to tools/scripts/profiling/clpeak/build.sh diff --git a/tools/profiling/clpeak/no_print.patch b/tools/scripts/profiling/clpeak/no_print.patch similarity index 100% rename from tools/profiling/clpeak/no_print.patch rename to tools/scripts/profiling/clpeak/no_print.patch diff --git a/tools/profiling/clpeak/run_continuously.patch b/tools/scripts/profiling/clpeak/run_continuously.patch similarity index 100% rename from tools/profiling/clpeak/run_continuously.patch rename to tools/scripts/profiling/clpeak/run_continuously.patch diff --git a/tools/profiling/ftrace.sh b/tools/scripts/profiling/ftrace.sh similarity index 100% rename from tools/profiling/ftrace.sh rename to tools/scripts/profiling/ftrace.sh diff --git a/tools/profiling/palanteer/.gitignore b/tools/scripts/profiling/palanteer/.gitignore similarity index 100% rename from tools/profiling/palanteer/.gitignore rename to tools/scripts/profiling/palanteer/.gitignore diff --git a/tools/profiling/palanteer/setup.sh b/tools/scripts/profiling/palanteer/setup.sh similarity index 100% rename from tools/profiling/palanteer/setup.sh rename to tools/scripts/profiling/palanteer/setup.sh diff --git a/tools/profiling/perfetto/.gitignore b/tools/scripts/profiling/perfetto/.gitignore similarity index 100% rename from tools/profiling/perfetto/.gitignore rename to tools/scripts/profiling/perfetto/.gitignore diff --git a/tools/profiling/perfetto/build.sh b/tools/scripts/profiling/perfetto/build.sh similarity index 100% rename from tools/profiling/perfetto/build.sh rename to tools/scripts/profiling/perfetto/build.sh diff --git a/tools/profiling/perfetto/copy.sh b/tools/scripts/profiling/perfetto/copy.sh similarity index 100% rename from tools/profiling/perfetto/copy.sh rename to tools/scripts/profiling/perfetto/copy.sh diff --git a/tools/profiling/perfetto/record.sh b/tools/scripts/profiling/perfetto/record.sh similarity index 100% rename from tools/profiling/perfetto/record.sh rename to tools/scripts/profiling/perfetto/record.sh diff --git a/tools/profiling/perfetto/server.sh b/tools/scripts/profiling/perfetto/server.sh similarity index 100% rename from tools/profiling/perfetto/server.sh rename to tools/scripts/profiling/perfetto/server.sh diff --git a/tools/profiling/perfetto/traces.sh b/tools/scripts/profiling/perfetto/traces.sh similarity index 100% rename from tools/profiling/perfetto/traces.sh rename to tools/scripts/profiling/perfetto/traces.sh diff --git a/tools/profiling/py-spy/profile.sh b/tools/scripts/profiling/py-spy/profile.sh similarity index 100% rename from tools/profiling/py-spy/profile.sh rename to tools/scripts/profiling/py-spy/profile.sh diff --git a/tools/profiling/snapdragon/.gitignore b/tools/scripts/profiling/snapdragon/.gitignore similarity index 100% rename from tools/profiling/snapdragon/.gitignore rename to tools/scripts/profiling/snapdragon/.gitignore diff --git a/tools/profiling/snapdragon/README.md b/tools/scripts/profiling/snapdragon/README.md similarity index 77% rename from tools/profiling/snapdragon/README.md rename to tools/scripts/profiling/snapdragon/README.md index f56ca182a7..383d83ba9b 100644 --- a/tools/profiling/snapdragon/README.md +++ b/tools/scripts/profiling/snapdragon/README.md @@ -4,10 +4,10 @@ snapdragon profiler * download from https://developer.qualcomm.com/software/snapdragon-profiler/tools-archive (need a qc developer account) * choose v2021.5 (verified working with 24.04 dev environment) -* unzip to selfdrive/debug/profiling/snapdragon/SnapdragonProfiler +* unzip to openpilot/selfdrive/debug/profiling/snapdragon/SnapdragonProfiler * run ```./setup-profiler.sh``` * run ```./setup-agnos.sh``` -* run ```selfdrive/debug/adb.sh``` on device +* run ```openpilot/selfdrive/debug/adb.sh``` on device * run the ```adb connect xxx``` command that was given to you on local pc * cd to SnapdragonProfiler and run ```./run_sdp.sh``` * connect to device -> choose device you just setup diff --git a/tools/profiling/snapdragon/setup-agnos.sh b/tools/scripts/profiling/snapdragon/setup-agnos.sh similarity index 100% rename from tools/profiling/snapdragon/setup-agnos.sh rename to tools/scripts/profiling/snapdragon/setup-agnos.sh diff --git a/tools/profiling/snapdragon/setup-profiler.sh b/tools/scripts/profiling/snapdragon/setup-profiler.sh similarity index 100% rename from tools/profiling/snapdragon/setup-profiler.sh rename to tools/scripts/profiling/snapdragon/setup-profiler.sh diff --git a/tools/profiling/watch-irqs.sh b/tools/scripts/profiling/watch-irqs.sh similarity index 100% rename from tools/profiling/watch-irqs.sh rename to tools/scripts/profiling/watch-irqs.sh diff --git a/selfdrive/debug/qlog_size.py b/tools/scripts/qlog_size.py similarity index 98% rename from selfdrive/debug/qlog_size.py rename to tools/scripts/qlog_size.py index 2b54cfeebf..46ef4b8be0 100755 --- a/selfdrive/debug/qlog_size.py +++ b/tools/scripts/qlog_size.py @@ -5,7 +5,7 @@ from collections import defaultdict import matplotlib.pyplot as plt -from cereal.services import SERVICE_LIST +from openpilot.cereal.services import SERVICE_LIST from openpilot.common.utils import LOG_COMPRESSION_LEVEL from openpilot.tools.lib.logreader import LogReader from tqdm import tqdm diff --git a/selfdrive/debug/run_process_on_route.py b/tools/scripts/run_process_on_route.py similarity index 100% rename from selfdrive/debug/run_process_on_route.py rename to tools/scripts/run_process_on_route.py diff --git a/tools/scripts/save_ubloxraw_stream.py b/tools/scripts/save_ubloxraw_stream.py deleted file mode 100755 index b5354a7831..0000000000 --- a/tools/scripts/save_ubloxraw_stream.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import os -import sys -from openpilot.common.basedir import BASEDIR -from openpilot.tools.lib.logreader import LogReader - -os.environ['BASEDIR'] = BASEDIR - - -def get_arg_parser(): - parser = argparse.ArgumentParser( - description="Unlogging and save to file", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - - parser.add_argument("route", type=(lambda x: x.replace("#", "|")), nargs="?", - help="The route whose messages will be published.") - parser.add_argument("--out_path", nargs='?', default='/data/ubloxRaw.stream', - help="Output pickle file path") - return parser - - -def main(): - args = get_arg_parser().parse_args(sys.argv[1:]) - - lr = LogReader(args.route) - - with open(args.out_path, 'wb') as f: - try: - done = False - i = 0 - while not done: - msg = next(lr) - if not msg: - break - smsg = msg.as_builder() - typ = smsg.which() - if typ == 'ubloxRaw': - f.write(smsg.to_bytes()) - i += 1 - except StopIteration: - print('All done') - print(f'Writed {i} msgs') - - -if __name__ == "__main__": - main() diff --git a/selfdrive/debug/set_car_params.py b/tools/scripts/set_car_params.py similarity index 94% rename from selfdrive/debug/set_car_params.py rename to tools/scripts/set_car_params.py index dabb6b7065..3864de0838 100755 --- a/selfdrive/debug/set_car_params.py +++ b/tools/scripts/set_car_params.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import sys -from cereal import car +from opendbc.car.structs import car from openpilot.common.params import Params from openpilot.tools.lib.route import Route from openpilot.tools.lib.logreader import LogReader diff --git a/tools/scripts/ssh.py b/tools/scripts/ssh.py index e454afb691..86e86c7eed 100755 --- a/tools/scripts/ssh.py +++ b/tools/scripts/ssh.py @@ -14,7 +14,7 @@ if __name__ == "__main__": parser.add_argument("device", help="device name or dongle id") parser.add_argument("--host", help="ssh jump server host", default="ssh.comma.ai") parser.add_argument("--port", help="ssh jump server port", default=22, type=int) - parser.add_argument("--key", help="ssh key", default=os.path.join(BASEDIR, "system/hardware/tici/id_rsa")) + parser.add_argument("--key", help="ssh key", default=os.path.join(BASEDIR, "openpilot/common/hardware/tici/id_rsa")) parser.add_argument("--debug", help="enable debug output", action="store_true") args = parser.parse_args() diff --git a/selfdrive/debug/test_fw_query_on_routes.py b/tools/scripts/test_fw_query_on_routes.py similarity index 100% rename from selfdrive/debug/test_fw_query_on_routes.py rename to tools/scripts/test_fw_query_on_routes.py diff --git a/selfdrive/debug/uiview.py b/tools/scripts/uiview.py similarity index 92% rename from selfdrive/debug/uiview.py rename to tools/scripts/uiview.py index 2cd541363c..f28ba3b2ec 100755 --- a/selfdrive/debug/uiview.py +++ b/tools/scripts/uiview.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 import time -from cereal import car, log, messaging +from openpilot.cereal import log, messaging +from opendbc.car.structs import car from openpilot.common.params import Params from openpilot.system.manager.process_config import managed_processes, is_tinygrad_model, is_stock_model -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE if __name__ == "__main__": CP = car.CarParams(notCar=True, wheelbase=1, steerRatio=10) diff --git a/tools/scripts/watch_timings.py b/tools/scripts/watch_timings.py new file mode 100755 index 0000000000..0df9db2033 --- /dev/null +++ b/tools/scripts/watch_timings.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +import argparse +import datetime +import time +from collections import deque +from dataclasses import dataclass, field + +import numpy as np + +import openpilot.cereal.messaging as messaging +from openpilot.cereal.services import SERVICE_LIST + + +@dataclass +class ServiceTiming: + times: list[float] = field(default_factory=list) + window: deque[float] = field(default_factory=lambda: deque(maxlen=100)) + valids: deque[bool] = field(default_factory=lambda: deque(maxlen=100)) + lag_events: list[tuple[float, float]] = field(default_factory=list) + + def add(self, mono_time: float, valid: bool, expected_interval: float | None, lag_threshold: float) -> None: + if self.times: + dt = mono_time - self.times[-1] + self.window.append(dt) + if expected_interval is not None and dt > lag_threshold * expected_interval: + self.lag_events.append((mono_time, dt)) + + self.times.append(mono_time) + self.valids.append(valid) + + def intervals(self, latest_only: bool) -> np.ndarray: + if latest_only: + return np.array(self.window) + return np.diff(self.times) + + +def format_row(name: str, timing: ServiceTiming, latest_only: bool) -> str: + dts = timing.intervals(latest_only) + if len(dts) == 0: + return f"{name:25} waiting for messages" + + mean = np.mean(dts) + hz = 1.0 / mean if mean > 0 else 0.0 + valid = all(timing.valids) if timing.valids else False + return f"{name:25} {hz:8.2f}Hz {mean * 1e3:8.2f}ms {np.std(dts) * 1e3:8.2f}ms {np.max(dts) * 1e3:8.2f}ms {np.min(dts) * 1e3:8.2f}ms valid={valid}" + + +def print_lag_events(name: str, timing: ServiceTiming, printed_lags: dict[str, int]) -> None: + start = printed_lags.get(name, 0) + for mono_time, dt in timing.lag_events[start:]: + print(f"{mono_time:.3f} {name} lag {dt:.3f}s", flush=True) + printed_lags[name] = len(timing.lag_events) + + +def monitor_services(socket_names: list[str], print_interval: float, lag_threshold: float, lag_only: bool) -> None: + sockets = {name: messaging.sub_sock(name, conflate=False) for name in socket_names} + timings = {name: ServiceTiming() for name in socket_names} + printed_lags: dict[str, int] = {} + + start_time = time.monotonic() + last_print = start_time + + try: + while True: + for name, sock in sockets.items(): + for msg in messaging.drain_sock(sock): + expected_interval = 1.0 / SERVICE_LIST[name].frequency if name in SERVICE_LIST else None + timings[name].add(msg.logMonoTime / 1e9, msg.valid, expected_interval, lag_threshold) + + now = time.monotonic() + if now - last_print < print_interval: + time.sleep(0.01) + continue + + if not lag_only: + print(flush=True) + print(f"{'service':25} {'freq':>10} {'mean':>10} {'std':>10} {'max':>10} {'min':>10} valid", flush=True) + for name in socket_names: + print(format_row(name, timings[name], latest_only=True), flush=True) + + for name in socket_names: + print_lag_events(name, timings[name], printed_lags) + + last_print = now + except KeyboardInterrupt: + print("\n", flush=True) + print("=" * 5, "timing summary", "=" * 5, flush=True) + print(f"{'service':25} {'freq':>10} {'mean':>10} {'std':>10} {'max':>10} {'min':>10} valid", flush=True) + for name in socket_names: + print(format_row(name, timings[name], latest_only=False), flush=True) + print("=" * 5, datetime.timedelta(seconds=time.monotonic() - start_time), "=" * 5, flush=True) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Check live service timing, frequency, validity, and lag") + parser.add_argument("socket", nargs="*", default=["carState"], help="service/socket name") + parser.add_argument("--lag-threshold", type=float, default=10.0, help="report intervals above this multiple of the expected service interval") + parser.add_argument("--lag-only", action="store_true", help="only print lag events") + parser.add_argument("--print-interval", type=float, default=1.0, help="seconds between table updates") + args = parser.parse_args() + + monitor_services(args.socket, args.print_interval, args.lag_threshold, args.lag_only) diff --git a/tools/setup_dependencies.sh b/tools/setup_dependencies.sh index 6cfd75ce17..b76fdc7dde 100755 --- a/tools/setup_dependencies.sh +++ b/tools/setup_dependencies.sh @@ -2,7 +2,7 @@ set -e DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -ROOT="$(cd "$DIR/../" && pwd)" +ROOT="$(git -C "$DIR" rev-parse --show-toplevel)" function retry() { local attempts=$1 @@ -44,7 +44,7 @@ function install_linux_deps() { echo "[ ] system packages already installed t=$SECONDS" elif command -v apt-get > /dev/null 2>&1; then $SUDO apt-get update - $SUDO apt-get install -y --no-install-recommends ca-certificates build-essential curl libcurl4-openssl-dev locales git + $SUDO apt-get install -y --no-install-recommends ca-certificates build-essential curl libcurl4-openssl-dev locales git xclip wl-clipboard elif command -v dnf > /dev/null 2>&1; then $SUDO dnf install -y ca-certificates gcc gcc-c++ make curl libcurl-devel glibc-langpack-en git elif command -v yum > /dev/null 2>&1; then diff --git a/tools/sim/bridge/__init__.py b/tools/sim/bridge/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tools/sim/lib/__init__.py b/tools/sim/lib/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tools/sim/tests/__init__.py b/tools/sim/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/uv.lock b/uv.lock index dc51d2a45e..a0a951d33d 100644 --- a/uv.lock +++ b/uv.lock @@ -2,100 +2,8 @@ version = 1 revision = 3 requires-python = ">=3.12.3, <3.13" -[[package]] -name = "acados" -version = "0.2.2" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=acados&rev=release-acados#25933c81c7db5573afe38f012f52befc15cdb804" } -dependencies = [ - { name = "numpy" }, -] - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" }, -] - -[[package]] -name = "aiohttp" -version = "3.13.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, - { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, - { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, - { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, - { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, - { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, - { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, - { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, - { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, - { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, - { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, - { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, - { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, - { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, -] - -[[package]] -name = "aioice" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dnspython" }, - { name = "ifaddr" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/04/df7286233f468e19e9bedff023b6b246182f0b2ccb04ceeb69b2994021c6/aioice-0.10.2.tar.gz", hash = "sha256:bf236c6829ee33c8e540535d31cd5a066b531cb56de2be94c46be76d68b1a806", size = 44307, upload-time = "2025-11-28T15:56:48.836Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/e3/0d23b1f930c17d371ce1ec36ee529f22fd19ebc2a07fe3418e3d1d884ce2/aioice-0.10.2-py3-none-any.whl", hash = "sha256:14911c15ab12d096dd14d372ebb4aecbb7420b52c9b76fdfcf54375dec17fcbf", size = 24875, upload-time = "2025-11-28T15:56:47.847Z" }, -] - -[[package]] -name = "aiortc" -version = "1.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aioice" }, - { name = "av" }, - { name = "cryptography" }, - { name = "google-crc32c" }, - { name = "pyee" }, - { name = "pylibsrtp" }, - { name = "pyopenssl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/51/9c/4e027bfe0195de0442da301e2389329496745d40ae44d2d7c4571c4290ce/aiortc-1.14.0.tar.gz", hash = "sha256:adc8a67ace10a085721e588e06a00358ed8eaf5f6b62f0a95358ff45628dd762", size = 1180864, upload-time = "2025-10-13T21:40:37.905Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/ab/31646a49209568cde3b97eeade0d28bb78b400e6645c56422c101df68932/aiortc-1.14.0-py3-none-any.whl", hash = "sha256:4b244d7e482f4e1f67e685b3468269628eca1ec91fa5b329ab517738cfca086e", size = 93183, upload-time = "2025-10-13T21:40:36.59Z" }, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, -] +[manifest] +overrides = [{ name = "opendbc", editable = "opendbc_repo" }] [[package]] name = "attrs" @@ -106,48 +14,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] -[[package]] -name = "av" -version = "16.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/cd/3a83ffbc3cc25b39721d174487fb0d51a76582f4a1703f98e46170ce83d4/av-16.1.0.tar.gz", hash = "sha256:a094b4fd87a3721dacf02794d3d2c82b8d712c85b9534437e82a8a978c175ffd", size = 4285203, upload-time = "2026-01-11T07:31:33.772Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/84/2535f55edcd426cebec02eb37b811b1b0c163f26b8d3f53b059e2ec32665/av-16.1.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:640f57b93f927fba8689f6966c956737ee95388a91bd0b8c8b5e0481f73513d6", size = 26945785, upload-time = "2026-01-09T20:18:34.486Z" }, - { url = "https://files.pythonhosted.org/packages/b6/17/ffb940c9e490bf42e86db4db1ff426ee1559cd355a69609ec1efe4d3a9eb/av-16.1.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ae3fb658eec00852ebd7412fdc141f17f3ddce8afee2d2e1cf366263ad2a3b35", size = 21481147, upload-time = "2026-01-09T20:18:36.716Z" }, - { url = "https://files.pythonhosted.org/packages/15/c1/e0d58003d2d83c3921887d5c8c9b8f5f7de9b58dc2194356a2656a45cfdc/av-16.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:27ee558d9c02a142eebcbe55578a6d817fedfde42ff5676275504e16d07a7f86", size = 39517197, upload-time = "2026-01-11T09:57:31.937Z" }, - { url = "https://files.pythonhosted.org/packages/32/77/787797b43475d1b90626af76f80bfb0c12cfec5e11eafcfc4151b8c80218/av-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7ae547f6d5fa31763f73900d43901e8c5fa6367bb9a9840978d57b5a7ae14ed2", size = 41174337, upload-time = "2026-01-11T09:57:35.792Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/d90df7f1e3b97fc5554cf45076df5045f1e0a6adf13899e10121229b826c/av-16.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8cf065f9d438e1921dc31fc7aa045790b58aee71736897866420d80b5450f62a", size = 40817720, upload-time = "2026-01-11T09:57:39.039Z" }, - { url = "https://files.pythonhosted.org/packages/80/6f/13c3a35f9dbcebafd03fe0c4cbd075d71ac8968ec849a3cfce406c35a9d2/av-16.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a345877a9d3cc0f08e2bc4ec163ee83176864b92587afb9d08dff50f37a9a829", size = 42267396, upload-time = "2026-01-11T09:57:42.115Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b9/275df9607f7fb44317ccb1d4be74827185c0d410f52b6e2cd770fe209118/av-16.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:f49243b1d27c91cd8c66fdba90a674e344eb8eb917264f36117bf2b6879118fd", size = 31752045, upload-time = "2026-01-11T09:57:45.106Z" }, -] - -[[package]] -name = "bootstrap-icons" -version = "1.10.5.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=bootstrap-icons&rev=release-bootstrap-icons#d7e41838af9320328dd7102f9288a94f0cf0fa9f" } - -[[package]] -name = "bzip2" -version = "1.0.8" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=bzip2&rev=release-bzip2#12916f9f84848239e5a69a8c5a282f3dcb0d86a3" } - -[[package]] -name = "capnproto" -version = "1.0.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=release-capnproto#a75eb83bd98440e890261f82aa8dcfbda0b0a206" } - -[[package]] -name = "catch2" -version = "2.13.10" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=catch2&rev=release-catch2#7287b4c99d23fdd87e66f9941b0e35ece6b4d91b" } - [[package]] name = "certifi" -version = "2026.5.20" +version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] @@ -200,14 +73,14 @@ wheels = [ [[package]] name = "click" -version = "8.4.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -228,6 +101,168 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "comma-deps-acados" +version = "0.2.2.post95" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/83/e76d3f89de07a672c5fc452d81b6f00f972721342eb75de2171d2c3a8b19/comma_deps_acados-0.2.2.post95-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fd7c583ac2a33b414540c0601173e3a9c28c5d8f825cb24736c64e3c07271f56", size = 10631724, upload-time = "2026-06-24T23:58:31.726Z" }, + { url = "https://files.pythonhosted.org/packages/64/96/4b8e50a153dcb5f34628f854dc58774588b8dbfb26b6bedd0b99acb4aa71/comma_deps_acados-0.2.2.post95-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1f47c3a665193937c993d3ee15989be5ba75eb427c9733fe1b9e800bf33c56aa", size = 11663713, upload-time = "2026-06-24T23:58:33.725Z" }, + { url = "https://files.pythonhosted.org/packages/1b/2f/bf57b9656e86950ba19c7a88992fccc0abb89878f9c30fb1b2252d57f0a7/comma_deps_acados-0.2.2.post95-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:00b49d5b691fa97d07fa5e5b842e0d2e1a6faa6856d2a081eefcee649f93329b", size = 13167209, upload-time = "2026-06-24T23:58:35.941Z" }, +] + +[[package]] +name = "comma-deps-bootstrap-icons" +version = "1.10.5.0.post95" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/0b/bb713dd4bed94b0b2b21657b7337e264953bd785a2b2f5c1b0706cdcde29/comma_deps_bootstrap_icons-1.10.5.0.post95-py3-none-any.whl", hash = "sha256:d59fc8d3e642e00f83d7a4854164f1dee21c3d17c726d5537b1b72b149f5788b", size = 386001, upload-time = "2026-06-24T23:58:37.84Z" }, +] + +[[package]] +name = "comma-deps-bzip2" +version = "1.0.8.post95" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/ac/539d76e9bcebbc53d62274a19c47522b118dce3261fc6baea045f93c98b6/comma_deps_bzip2-1.0.8.post95-py3-none-macosx_11_0_arm64.whl", hash = "sha256:be466743b7fc56fa18c2c389a46bdeb0fc885cf3cf39b779d748ce540609ebd5", size = 42832, upload-time = "2026-06-24T23:58:39.592Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/5206bd6d716022cea259c4878c6a1927ab3555f4caa48a74efaa83d8f418/comma_deps_bzip2-1.0.8.post95-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:7c015d720cc5462f87062b4482c54684de6a237b4edb60612c2f58721413682f", size = 38629, upload-time = "2026-06-24T23:58:40.496Z" }, + { url = "https://files.pythonhosted.org/packages/39/0e/78218f9a645ad9d27000153795d6819b3b52ca62d882aa46a413e40887be/comma_deps_bzip2-1.0.8.post95-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3188a197aaf3efbfac975193887c59600b91a3b4eb7f62cc975377133cd48834", size = 36271, upload-time = "2026-06-24T23:58:41.2Z" }, +] + +[[package]] +name = "comma-deps-capnproto" +version = "1.0.1.post93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/a6/97c22a112e23f530db28f8ecf7b191ec16685d282fdbe892dff7437bdf6d/comma_deps_capnproto-1.0.1.post93-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6717e8e34ef12116502f244b23f092984c1d15afa4e40b956450b74a0d4d2520", size = 2407330, upload-time = "2026-07-08T19:30:41.395Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f2/79a15ff5f2c97a741923a97357f6a4235e4ced928ba1d4079d01c66df4b0/comma_deps_capnproto-1.0.1.post93-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a70349446e2ec17b281ebaaae02ca4c35495f4210043c716c55139a05a826090", size = 2506341, upload-time = "2026-07-08T19:30:45.607Z" }, + { url = "https://files.pythonhosted.org/packages/80/a9/f61fe62045c4ea867f51f2d74b4edfabd6c272c62a3281a9f1f33825a725/comma_deps_capnproto-1.0.1.post93-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:f98cdba8f8c7f08a7a0c0a4b7cc0bfcf515e29ad8f1b5cb8eda4e253bef5e6e3", size = 2590769, upload-time = "2026-07-08T19:30:49.728Z" }, +] + +[[package]] +name = "comma-deps-catch2" +version = "2.13.10.post93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ee/b4ef7758d04a024775d49558ca930fec19aa37cd691ad9182b7141f4d4d7/comma_deps_catch2-2.13.10.post93-py3-none-any.whl", hash = "sha256:8f23293251b5db48c08885d8816ca252b3fb11b0c1c26775bae36e8b217e865e", size = 137085, upload-time = "2026-07-08T19:30:53.44Z" }, +] + +[[package]] +name = "comma-deps-eigen" +version = "3.4.0.post93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/3c/b262d6103a3b7a5cb0296b821eb4f1385b349e527bba75d7efe057cea802/comma_deps_eigen-3.4.0.post93-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f90433cea3f59f1b9ed58bd92d0f1b55721eff8b473d579ed40fa2fcdc2c1787", size = 2275895, upload-time = "2026-07-08T19:31:08.983Z" }, + { url = "https://files.pythonhosted.org/packages/00/a1/b9bf332a096267ff26ebd546b104d9a92b905857c59bae899794897c790b/comma_deps_eigen-3.4.0.post93-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:3169d82b825ca8accdae5f3e1a9cddd9f28f8bd5f66e4353fedae68b23f13cdb", size = 2275897, upload-time = "2026-07-08T19:31:12.66Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c6/1132467acd1257ea159cf4608c6f00ef0b3ca353fd86aaced0d76dcce62c/comma_deps_eigen-3.4.0.post93-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:d0b042c817070a0124b8c0e78b87ff5e8b44db21a1abea740afe7115f5bd8447", size = 2275900, upload-time = "2026-07-08T19:31:16.352Z" }, +] + +[[package]] +name = "comma-deps-ffmpeg" +version = "7.1.0.post94" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/5c/47e10049fd96b581e8ddc9fba31c3397562732fcc99db6c062690e862300/comma_deps_ffmpeg-7.1.0.post94-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e87b4dd0fb0fc25d6992167ee266b5c9e92305aefa3d343ddd22a17219363b8", size = 7329953, upload-time = "2026-07-09T03:07:11.516Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d5/c4edfbbe488983ff45fb6cacab78342fc56cc0f28010a946e8697613ffe2/comma_deps_ffmpeg-7.1.0.post94-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:7ea34368c5b564cecab0fd77d6423bc15d8f100051648f64ad79a3499f43dafb", size = 4441314, upload-time = "2026-07-09T03:07:15.679Z" }, + { url = "https://files.pythonhosted.org/packages/80/8f/76a876a26572c52d52dbcf8ed39227f5023d343201a32af4b992c4b9f13e/comma_deps_ffmpeg-7.1.0.post94-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:5b2d6cfbba59ecd673dd0c8bd8eb5a5b3472bdfa025e1063c011e6234877fe84", size = 4685134, upload-time = "2026-07-09T03:07:19.596Z" }, +] + +[[package]] +name = "comma-deps-gcc-arm-none-eabi" +version = "13.2.1.post93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/9f/9a6c7f04faf18fdc4ab3e5b55ecfdbcb3998333a450fd5a1c986af571d15/comma_deps_gcc_arm_none_eabi-13.2.1.post93-py3-none-macosx_11_0_arm64.whl", hash = "sha256:00de06a94ecf4379c79b150d36b3116d0f37bc341bbf1ef2ad850c8a5985eeb1", size = 15238809, upload-time = "2026-07-08T19:31:33.509Z" }, + { url = "https://files.pythonhosted.org/packages/83/94/1cdb11c170b96f2677dfd1be7cfb9a2994d3518555c154f2a131929057da/comma_deps_gcc_arm_none_eabi-13.2.1.post93-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:de1cf3ff89ea84fac5df62c65d843af877eaa2634439d793f527f0cfeea66e59", size = 17367238, upload-time = "2026-07-08T19:31:38.137Z" }, + { url = "https://files.pythonhosted.org/packages/23/3c/4b2f60274f080b9583da2fdaad54f692f4f8719bd360558d4460d79f8fb4/comma_deps_gcc_arm_none_eabi-13.2.1.post93-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3cf7f022ffa1b3d3e20eb32610c7e59f84932e516d9ed11236df787546d82d67", size = 16941134, upload-time = "2026-07-08T19:31:43.102Z" }, +] + +[[package]] +name = "comma-deps-git-lfs" +version = "3.6.1.post93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/62/e06ea6fb98b8cee2686fb409fb28426d902f3318b077cbd78d9bbec0354b/comma_deps_git_lfs-3.6.1.post93-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89cbffd3d6800fa8e366301b98c09c4ccbc973fb59721aa2546b36158b9b64c0", size = 4685107, upload-time = "2026-07-08T19:31:47.273Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cf/f08dc359e8bc7e1b21b7f4a93167a14ac61dc78f1bfb27a0e7554c78cb09/comma_deps_git_lfs-3.6.1.post93-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:39844960733a3885d99bbfdf4f7b414e7d0bf4b4d56f9d77a88ea10d72f549c5", size = 4485276, upload-time = "2026-07-08T19:31:51.253Z" }, + { url = "https://files.pythonhosted.org/packages/2f/af/3dfae2a56320165b45ceab0d7a2c6dea69cc5766928f408d17997ff2a525/comma_deps_git_lfs-3.6.1.post93-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:e445cbadec41856997de775d5d415d1e707db0ba563c705df1b55ce198179bbe", size = 4889583, upload-time = "2026-07-08T19:31:55.153Z" }, +] + +[[package]] +name = "comma-deps-imgui" +version = "1.92.7.post93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/e8/a5f99714d4b2ef85744bd6af9ee46b016a70cea8383c9cca3097847c6940/comma_deps_imgui-1.92.7.post93-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ae7cbb655cd61157b785e7f388cc1bfe17bf6a2bf8c40ff9484702bd9be71074", size = 1688022, upload-time = "2026-07-08T19:31:58.849Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/0ebcbb7d55ef1ad0f516831c52f37cafdf0ed94b73e0396c531ecd89dbc1/comma_deps_imgui-1.92.7.post93-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:efc689e8279900c10e3922e9bf188fe34e16ab28d8660878e82b7192d7d86c68", size = 2522798, upload-time = "2026-07-08T19:32:02.435Z" }, + { url = "https://files.pythonhosted.org/packages/c3/94/6f0bd31d599f5748736b57e0b71a42d15ec0782906155f268bc7056fd0ac/comma_deps_imgui-1.92.7.post93-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:b08a763c00a1f037dc5e2e2e0322c46960fe8a2cc972ce17781d547ccce3eaa6", size = 2655457, upload-time = "2026-07-08T19:32:06.273Z" }, +] + +[[package]] +name = "comma-deps-json11" +version = "20170411.0.post93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/3e/f2bb5d0e0d9e63535007c64859b2ab5e17164ca1f4cfa279fd228dbcef80/comma_deps_json11-20170411.0.post93-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0088d66fd76ea27712d09f2ffa783596f7d70b5b152a039c36d65befcebe3863", size = 34033, upload-time = "2026-07-08T19:32:10.175Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6e/9576bb1ba5d8371f32664268401fa61231ca1092aa5da45141c37984d868/comma_deps_json11-20170411.0.post93-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:659c18d4b2634bf7d2388c6e855c6f083f384d2a70052945c1c9ddabbb0dcb21", size = 41846, upload-time = "2026-07-08T19:32:13.457Z" }, + { url = "https://files.pythonhosted.org/packages/cc/34/6d673c311c9a23e65a0db56352878b53b575aca0539e57c71121d2fe544b/comma_deps_json11-20170411.0.post93-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:4c24a9c9db71b38192ec9c515f13fd14b4495b8080bb1f687b84b193613d95be", size = 42603, upload-time = "2026-07-08T19:32:16.601Z" }, +] + +[[package]] +name = "comma-deps-libusb" +version = "1.0.29.post93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/a3/4fb912d8af7af134a0a54eeb893beecc9e1cb3ed8371e055319faa74a580/comma_deps_libusb-1.0.29.post93-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1dba5fe7832877994ea9ff3daf056851f1b5c9a47552525af08b35074c5ff65e", size = 102339, upload-time = "2026-07-08T19:32:19.785Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ad/6d36327352fa1030997409ee650de3f307cc26e3fec4f2a8ea6254430e6a/comma_deps_libusb-1.0.29.post93-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:c2a29c888668cb4c0ab549ac71206d6b53da8d2db5e581a55a113574aaabe950", size = 94439, upload-time = "2026-07-08T19:32:23.104Z" }, + { url = "https://files.pythonhosted.org/packages/84/47/f4f2da67db202bacc3fa578fdebdb916f45c97bee0c3d2c5fbe845a6f129/comma_deps_libusb-1.0.29.post93-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:f136dbbfc461d228378731361cc66a90a483e17d55f8ba43f23eeab6bcf41963", size = 93461, upload-time = "2026-07-08T19:32:26.434Z" }, +] + +[[package]] +name = "comma-deps-ncurses" +version = "6.5.post93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/3d/f85c036037e9d72c8cc444a1613ae8ca6967c0997148fbc8c03b8cdeb21c/comma_deps_ncurses-6.5.post93-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0fbc94bdb42577389ec9e475deb7425f027b4e6a097b7b326ee0ee3ecb4c26ff", size = 264892, upload-time = "2026-07-08T22:08:58.325Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/13881cefe9ca963665a57f066683fd9995c15349839f0268686741942cd7/comma_deps_ncurses-6.5.post93-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a8786c9e7693fbe235044ed068292ddae1e8c25e9cc3ffba9364c233b6df9f9c", size = 260837, upload-time = "2026-07-08T22:09:02.514Z" }, + { url = "https://files.pythonhosted.org/packages/11/98/6a278e4436ca901c084ae111dc23f064e3eb5f64481dc970111aff8a91fd/comma_deps_ncurses-6.5.post93-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:1de1011e71833d46d545a0d64150316f980385d13731eb138f9fc53642942c55", size = 248341, upload-time = "2026-07-08T22:09:06.591Z" }, +] + +[[package]] +name = "comma-deps-raylib" +version = "6.0.0.1.post93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/5a/1abdc781fc242e0db517e22c53fa676fc81b76344c5d6c86677bdd5d6a9a/comma_deps_raylib-6.0.0.1.post93-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ef4dbc7036e4449f9ee029cec077c23f64975b55b60cce0be818b70640c1732a", size = 2004199, upload-time = "2026-07-08T22:09:10.731Z" }, + { url = "https://files.pythonhosted.org/packages/79/90/919fd98aa72c8c1d31e657abc0bc28b3933d7408f4dc8ccbb7272ef638a9/comma_deps_raylib-6.0.0.1.post93-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a64f2a77ad55b05350ed4301f3a1a814176810d1d408c77bdbaa1bd5a3ab5135", size = 7203010, upload-time = "2026-07-08T22:09:15.323Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b6/419b387b132efe43ace20dced9852448f36195d71d1305fccfd5e0617083/comma_deps_raylib-6.0.0.1.post93-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:11bf076441b636434bc76a00b2e349c22a825e0b83416a4fb3dd4b8217638f8a", size = 5055450, upload-time = "2026-07-08T22:09:19.844Z" }, +] + +[[package]] +name = "comma-deps-zeromq" +version = "4.3.5.post93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/6f/d50752a3a6f6ecd4b4289bf76090fd35c11ac8d4b9353534e91e9a640512/comma_deps_zeromq-4.3.5.post93-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b5951b302ec3db7ad3638161af00dff6ad1d8e0272aa242a0b57d692a093bacc", size = 815166, upload-time = "2026-07-08T22:09:24.48Z" }, + { url = "https://files.pythonhosted.org/packages/57/24/0fa08db746438533a67e44886bffb1cd7e2a587c68d57fba745e84036965/comma_deps_zeromq-4.3.5.post93-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a7d4813155ea13548158204a60e6a37e2dedfcf7ae34cbb78491474ac784ac24", size = 833412, upload-time = "2026-07-08T22:09:28.872Z" }, + { url = "https://files.pythonhosted.org/packages/c5/83/9083466d8269bf273a7ff4ff154fbcd300a8d9c79ca7754fb98215caf602/comma_deps_zeromq-4.3.5.post93-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:378f121eea1084c7cd45ee73c3dbc437e3d07547e8333d230174711e963f958d", size = 798729, upload-time = "2026-07-08T22:09:32.859Z" }, +] + +[[package]] +name = "comma-deps-zstd" +version = "1.5.6.post93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/32/e62e39d77d356675b07fa0c03286d3f5620bb39fe8619285bfaf3c085f6b/comma_deps_zstd-1.5.6.post93-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9c40c10fa541c5ed975a06327f4e5cc5fe380d49c50fd2899f367c52e0d535a1", size = 1065092, upload-time = "2026-07-08T22:09:37.023Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f9/80e530ffed74b767591e50367dd51413c99e8aa10a975745086f2f10eac8/comma_deps_zstd-1.5.6.post93-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:3a1da859191a9b4c778498afdaa80a7000e5967d4851d827e422c467aa159f9a", size = 1006181, upload-time = "2026-07-08T22:09:41.332Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b1/5b544af0a50235b1efe9a9de469180ded30814feb8e6a2cd89d4064f995e/comma_deps_zstd-1.5.6.post93-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:fe85e5fd24ac81964cac30ad04327ae157b5976a69725604ec960e773f06a9a8", size = 1030331, upload-time = "2026-07-08T22:09:45.535Z" }, +] + [[package]] name = "contourpy" version = "1.3.3" @@ -252,81 +287,63 @@ wheels = [ [[package]] name = "coverage" -version = "7.14.1" +version = "7.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/8b/adeb62ea8951f13c4c7fef2e7a85e1a06b499c8d8237ea589d496029e53f/coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f", size = 925362, upload-time = "2026-07-02T13:10:50.535Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c", size = 220022, upload-time = "2026-05-26T20:39:03.702Z" }, - { url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" }, - { url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" }, - { url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" }, - { url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" }, - { url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" }, - { url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" }, - { url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" }, - { url = "https://files.pythonhosted.org/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1", size = 222567, upload-time = "2026-05-26T20:39:24.868Z" }, - { url = "https://files.pythonhosted.org/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e", size = 223372, upload-time = "2026-05-26T20:39:26.603Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a", size = 221989, upload-time = "2026-05-26T20:39:28.242Z" }, - { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, -] - -[[package]] -name = "crcmod-plus" -version = "2.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/0c/71733bbaf38e9f1eaecfdf7f8e350993f3dcac208a5297c41503ae66e513/crcmod_plus-2.3.1.tar.gz", hash = "sha256:732ffe3c3ce3ef9b272e1827d8fb894590c4d6ff553f2a2b41ae30f4f94b0f5d", size = 22319, upload-time = "2025-10-10T22:14:21.691Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/e0/2dad2e6f0cd4914b4144496d9785780ec820e200816c080df785cfa34da6/crcmod_plus-2.3.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:b7e35e0f7d93d7571c2c9c3d6760e456999ea4c1eae5ead6acac247b5a79e469", size = 23279, upload-time = "2025-10-10T22:13:47.281Z" }, - { url = "https://files.pythonhosted.org/packages/66/76/53c0b65b9679b903f98fc54efa32b0e5a19634712a45200c7a80674aa6f5/crcmod_plus-2.3.1-cp311-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6853243120db84677b94b625112116f0ef69cd581741d20de58dce4c34242654", size = 20185, upload-time = "2025-10-10T22:13:48.06Z" }, - { url = "https://files.pythonhosted.org/packages/98/79/2b4dc9bb26394873d7699737124408b5106264ae33053fdec600e9a9fa65/crcmod_plus-2.3.1-cp311-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:17735bc4e944d552ea18c8609fc6d08a5e64ee9b29cc216ba4d623754029cc3a", size = 26999, upload-time = "2025-10-10T22:13:48.854Z" }, - { url = "https://files.pythonhosted.org/packages/bb/e8/f5d66778b5a1bff915807016561a02b5cebf6b3840fb8a2be40bbb0c8575/crcmod_plus-2.3.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac755040a2a35f43ab331978c48a9acb4ff64b425f282a296be467a410f00c3", size = 27536, upload-time = "2025-10-10T22:13:49.956Z" }, - { url = "https://files.pythonhosted.org/packages/f3/2c/0113ad30cadad40c22eef08c0f2618f2446dd282f02268fecbcfc9fda3c1/crcmod_plus-2.3.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bdcfb838ca093ca673a3bbb37f62d1e5ec7182e00cc5ee2d00759f9f9f8ab11", size = 27385, upload-time = "2025-10-10T22:13:50.765Z" }, - { 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/2a/74/fd4c0901137c4f8d81a76ada99e43c65163b4c94a02ece107a4ec0c6b615/coverage-7.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b75ee5e8cb7575636ac598719b4307ac529ec8fcd79608a35c3cd4d4dada812d", size = 220838, upload-time = "2026-07-02T13:09:02.084Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2e/2347583467bd7f0402635101a916961915cc68fce652cd0db5f173ea04fc/coverage-7.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffb31267816b93b075302248cc1737506081b4f163df4401e9df1a6424aafabe", size = 221197, upload-time = "2026-07-02T13:09:03.617Z" }, + { url = "https://files.pythonhosted.org/packages/f0/17/99fa688541ae1d6e84543a0e544f83de0c944815b63e9e7b1ed411d15036/coverage-7.15.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4d0bb73455bf97ab243a8f12c37c686ccf1c13bb614b7b85f1d062f06f42b2c", size = 252705, upload-time = "2026-07-02T13:09:05.059Z" }, + { url = "https://files.pythonhosted.org/packages/fb/02/6a95a5cd83b74839017ef9cf48d2d8c9ae60af919e17a3f336e6f9f1b7bd/coverage-7.15.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:20d9ccc4ebd0edc434d86dfd2a1dd2a8efa6b6b3073d0485a394fee86459ebb4", size = 255441, upload-time = "2026-07-02T13:09:06.559Z" }, + { url = "https://files.pythonhosted.org/packages/67/f2/406f6c57d600f68185942422c4c00f1a3255d60aee6e5fd961425cd9987e/coverage-7.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20c8a976c365c8cb12f0cbd099508772ea41fb5fa80657a8506df0e11bd278c5", size = 256556, upload-time = "2026-07-02T13:09:08.197Z" }, + { url = "https://files.pythonhosted.org/packages/74/8e/d3fa48489c15ecdec1ba48fd61f68798555dddd2f6716f9ad42adeb1a2a9/coverage-7.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f948fd5ba1b9cbca91f0ae08b4c1ce2b139509149a435e2585d056d57d70bf01", size = 258815, upload-time = "2026-07-02T13:09:09.691Z" }, + { url = "https://files.pythonhosted.org/packages/47/2e/2d40ddd110462c6a2769677cf7f1c119a52b45f568978fc6c98e4cc0dd0f/coverage-7.15.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f58185f06edf6ad68ec9fb155d63ef650c82f3fbd7e1770e2867751fb13158f4", size = 253117, upload-time = "2026-07-02T13:09:11.212Z" }, + { url = "https://files.pythonhosted.org/packages/51/c0/310782f0d7c3cb2b5ac05ba8d205fe91f24a36f6bf3256098f1782181c38/coverage-7.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02adc79a920c73c647c5d117f55747df7f2de94571884758ce8bc58e04f0a796", size = 254475, upload-time = "2026-07-02T13:09:13.029Z" }, + { url = "https://files.pythonhosted.org/packages/86/f7/702da6c275f8ae6ade423d2877243122932c9b27f5403003b9ef8c927d12/coverage-7.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6eb7c300fbed667fd6e3588eba71c1904cdb06110ca6fdf908c26bdd88b8e382", size = 252619, upload-time = "2026-07-02T13:09:14.699Z" }, + { url = "https://files.pythonhosted.org/packages/fb/84/c5b15a7e5ecba4e56218d772d99fe80a63e63f8d11f12783723a6005ab45/coverage-7.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b5fb23fa2de9dce1f5c36c09066d8fcda16cd96e8e26686caa2d7cb9b567d65c", size = 256689, upload-time = "2026-07-02T13:09:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/95/2f/c8b07559b57701230c61b23a953858c052890c12ef568d81780c6c46e92e/coverage-7.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cec79341dbe6281484024979976d0c7f22beae08b4a254655decd25d42cbe766", size = 252189, upload-time = "2026-07-02T13:09:17.828Z" }, + { url = "https://files.pythonhosted.org/packages/6b/80/6d2f049dd3fd3dbfd60b62ba6b2162a04009e2c002ce70b24cf3878dec7a/coverage-7.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c664c5444b1d970b1b2a450e21fb19ee5c9cfdf151ded2dda37260031cca0da", size = 254059, upload-time = "2026-07-02T13:09:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/ce/92/b0287a2c42031d25c628f815f89a3cd9f8268ee78bb1252c9356cda1c689/coverage-7.15.0-cp312-cp312-win32.whl", hash = "sha256:5f764a3fa339bde6b3aa97657f5a6a3a9451e4a5b4ea98a2892c773a43525f77", size = 222893, upload-time = "2026-07-02T13:09:20.812Z" }, + { url = "https://files.pythonhosted.org/packages/a9/69/e34c481915fecb499b3146975061dac528752e37706edc1804f32c822469/coverage-7.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:52f9a4d2c4c56c8848bc2f524916698354b0211488b38c49ad9ae54f6cafbff6", size = 223429, upload-time = "2026-07-02T13:09:22.315Z" }, + { url = "https://files.pythonhosted.org/packages/fe/98/6e878f0b571d32684ef3f38d7c03db241ca5b82a5da8a5391596a8f209c4/coverage-7.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:31e5c3e70c85307ea35a12964e2e40f56ca2ee4b1c8c721ccf4609d17071080b", size = 222810, upload-time = "2026-07-02T13:09:23.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/30/21b2ad45959cd50e909e02ebac1e30b4ceb7162e91c11d4c570223a458b7/coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19", size = 212632, upload-time = "2026-07-02T13:10:48.641Z" }, ] [[package]] name = "cryptography" -version = "48.0.0" +version = "49.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, - { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, - { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, - { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, - { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, - { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, - { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, - { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, - { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, ] [[package]] @@ -340,49 +357,35 @@ wheels = [ [[package]] name = "cython" -version = "3.2.5" +version = "3.2.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/3b/ebd94c8b85f8e41b5015a9ed94ee3df866024d480d05cd08b774684fb81d/cython-3.2.5.tar.gz", hash = "sha256:3dd42e4cf36ad15f265bdfec2337cc00c688c8eb6d374ffd13bb19437c27bba1", size = 3286381, upload-time = "2026-05-23T19:34:08.439Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/6b/80101e02ebacaf9232ecf32bf6a788d36b27d820ee02434746252569ef98/cython-3.2.8.tar.gz", hash = "sha256:f4f23a56b25221a06f91817fe8f3114ab8b48a4fac73187dbb64bc2c4a87961f", size = 3290300, upload-time = "2026-06-30T07:41:57.874Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/a6/efc97000fdb2f34e2431eb09a6ab4de9fbd3bcdb73a8f9d224afa4a9abd3/cython-3.2.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eb38b89e5a8eb2508a1a0832063826b0703dfb02be84e4aa34b8818ce0ca50fe", size = 2979670, upload-time = "2026-05-23T19:34:41.281Z" }, - { url = "https://files.pythonhosted.org/packages/84/b7/951206add609c11f3bb9e82329a653c39a8bc9039c13bce57362caf84bb6/cython-3.2.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80e1e5cba5b4b9890364e9360939fc298c474f25754bb4bb861270d24bda6d6", size = 3232779, upload-time = "2026-05-23T19:34:43.347Z" }, - { url = "https://files.pythonhosted.org/packages/a1/aa/8a1d02eabe8bc1e5066fde920010a4a4a4c5f0bac3625d8e7c946f72ef98/cython-3.2.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e2c976ee96da4deff50506c7882ccebb4a932fc178ef27eb42bfde959839", size = 3400054, upload-time = "2026-05-23T19:34:45.6Z" }, - { url = "https://files.pythonhosted.org/packages/57/30/67a1b6192c828456f096d4bf4d840b9a749904b9030d9f857549fc1f9b53/cython-3.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:29243859d6824e2d33bae92fc83d591c3671b6d9ac1b757fa264b894ae906c2b", size = 2759539, upload-time = "2026-05-23T19:34:47.341Z" }, - { url = "https://files.pythonhosted.org/packages/a3/de/e3e0cf5704fe569d54b8cd5dc316c9fbf08b1b74728732f86e90168b7a3f/cython-3.2.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:224149d18d980e6ea5001b70fc7ce096c1891d59035dfa9cc5ede50f55804913", size = 2879054, upload-time = "2026-05-23T19:35:18.265Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d1/0a6a8caa35c4c57a1f1866b1141c2d00c6af67f73edbe34b2baec6919ccf/cython-3.2.5-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:992a50e90d01813333752f374a4405863113059ec67102ab8d6a431a171ee328", size = 3210422, upload-time = "2026-05-23T19:35:20.641Z" }, - { url = "https://files.pythonhosted.org/packages/07/b8/2523398ec96bb0c9bf69ada625a2256a581940b09fe11fcd0029f26ef4ad/cython-3.2.5-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8d7b81e6a52a84a02993f01aa5873786ba1dd593c892d93d5fe9866da0bad297", size = 2863809, upload-time = "2026-05-23T19:35:22.416Z" }, - { url = "https://files.pythonhosted.org/packages/ff/3d/6b2f316d97bdb02283d79934e50da5cedfec65a536cdd3d69cc3a93486f9/cython-3.2.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:34d21aeb08477c9173e8be7a566b19e880a7c8109ec6bb47a4b20cb680141114", size = 2992518, upload-time = "2026-05-23T19:35:24.737Z" }, - { url = "https://files.pythonhosted.org/packages/68/2c/c9238db1eba208e226d363c00c8b74bf531a6b40c75df2334baa85e142bf/cython-3.2.5-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c4c79e697db55f082a2d3ba97702e71881d5bb1f56f0a80fa338e69101e4c59b", size = 2886221, upload-time = "2026-05-23T19:35:26.64Z" }, - { url = "https://files.pythonhosted.org/packages/2d/15/229cc5c2ed92bb8b43c73a3d31c2b4eaf498409300c34a06d93147f7a42b/cython-3.2.5-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:39acb30eba78ba6d995d5cf3d97d57d450663d93aac6f8b93753d2b89d768c60", size = 3226990, upload-time = "2026-05-23T19:35:28.979Z" }, - { url = "https://files.pythonhosted.org/packages/56/31/9c0024f2c772fc303f8cae2a204bcad2fedfaf921ba71cf13a878639432d/cython-3.2.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:382122de8d6b6024fc374fabc3a2b14ba5860ed981c25055ed14fe44278b9dc7", size = 3111004, upload-time = "2026-05-23T19:35:30.957Z" }, - { url = "https://files.pythonhosted.org/packages/82/71/8b528247e42ee63cbe1c1d53805d30b28663fa782c88da4a9b69a1a412dd/cython-3.2.5-cp39-abi3-win32.whl", hash = "sha256:0bc29c7f870b09efdb1f583fbec9592b33af81a7ce273b89c8f5163d7572d5c1", size = 2440395, upload-time = "2026-05-23T19:35:33.082Z" }, - { url = "https://files.pythonhosted.org/packages/50/4d/81c91d3279d156ee2c9ead7ed9eaa862e498066d759e92fb83d0d842c5a7/cython-3.2.5-cp39-abi3-win_arm64.whl", hash = "sha256:85b2944c3eddfc230f9082720195a2e9f869908e5a8b3185be1be832755ee7fc", size = 2446963, upload-time = "2026-05-23T19:35:35.267Z" }, - { url = "https://files.pythonhosted.org/packages/d4/5c/9cd909e6a8bb178e4e0f9a2a9227c8201a2be38abe45ada4a4c3e9154277/cython-3.2.5-py3-none-any.whl", hash = "sha256:dc1c8cebb7df5bce37f5f8dc1e5bf04313272a5973d50a55c0ec76c83812911b", size = 1257622, upload-time = "2026-05-23T19:34:05.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c4/47e0bcfc15b36b1c5cbde5235c60bf88df552ab216ddb836d7f816386ae6/cython-3.2.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2547a31fbd3b1610a8859a16edee2a141f7781691cb98a2c6fd54870c5f7541", size = 2995546, upload-time = "2026-06-30T07:42:23.618Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f4/bc5830abeb57a7c7498cd9a0f2df953fd9fc7f33e3f5352c9824802b83bb/cython-3.2.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab1fe11ebd61e497a848622cdd157a4324ac06e7935b1219715408844e15dd13", size = 3179546, upload-time = "2026-06-30T07:42:25.566Z" }, + { url = "https://files.pythonhosted.org/packages/89/38/a70e879ea52debac11d2810e066a5a2cb16e71229edae303f024506bc142/cython-3.2.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3bce1f079734753649f8a3d3a95832297207feb120d0ef4fd5db4c813cc6c04", size = 3351714, upload-time = "2026-06-30T07:42:27.513Z" }, + { url = "https://files.pythonhosted.org/packages/45/f1/f071c5e7050a7924ffad9822558c74d489afe4764f7486cb68555a509219/cython-3.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:8297efe129e6421c34ddbeb09ed5627ef6c0fc4868bf7f9bdf6c147f595ccaed", size = 2774196, upload-time = "2026-06-30T07:42:29.47Z" }, + { url = "https://files.pythonhosted.org/packages/92/a2/0f2eaa5076bcaef52567471a54ce02ffd70007bf8688cd054f7aab9bc3b8/cython-3.2.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:127bc4039be48c6eebe7f1d68c33d23eb3a0c5ae95e1d730bdd048837751438b", size = 2892527, upload-time = "2026-06-30T07:42:55.45Z" }, + { url = "https://files.pythonhosted.org/packages/a4/08/b5488aef44662e48ac09b42d4cb398207f591c770797036fb1d6fbeb7a52/cython-3.2.8-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e480ae9f195cd29e5ce334e3d434c83dbac0783c0cc88f2407e31ba997724192", size = 3220335, upload-time = "2026-06-30T07:42:57.403Z" }, + { url = "https://files.pythonhosted.org/packages/7c/96/d04a3621045e9fe9c7c5e406a688ee3d6e04a65f545ea7c622ead4b4afd8/cython-3.2.8-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3142407b9d63f233c766e17000e2aac782411bf0409b9adc97cb7c320aecd199", size = 2876481, upload-time = "2026-06-30T07:42:59.406Z" }, + { url = "https://files.pythonhosted.org/packages/71/9a/daa259b638c5eabb8a8c36f203b85b01b5362101ff8fc4ec6ad592d34bb3/cython-3.2.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:41c118fd91d320cd72af26e29232ff3f1a0a170c47d477c9a176d766067a4718", size = 2999974, upload-time = "2026-06-30T07:43:01.29Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/71cc5b4be5ee4d34c3302b6e7272189106a4072e9890d284d05239d2b645/cython-3.2.8-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6335c6e8737a39734e20d25f89f425bf3274c104fc7efc05aacc7ed1a4858c9d", size = 2897932, upload-time = "2026-06-30T07:43:03.606Z" }, + { url = "https://files.pythonhosted.org/packages/5d/0c/da68b9d3056e90b2060970b50d575cd7fcd1c778e8f23cc467f346e1d471/cython-3.2.8-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1034facc082cb882e1e5beb61e136ae8e282df2eafa11e9771e9b0a15c860801", size = 3235980, upload-time = "2026-06-30T07:43:05.486Z" }, + { url = "https://files.pythonhosted.org/packages/36/0b/d88bc50e66fd1f1160dd2677d9af18273a2fb2f102c086d21f64a5a9c78b/cython-3.2.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8896ff6b133f346ebcf22aa23706c4031e8d9d5ae184433dfc67dc1053318b69", size = 3118504, upload-time = "2026-06-30T07:43:07.485Z" }, + { url = "https://files.pythonhosted.org/packages/db/de/511c4364808b3b4036d051a0301b0b142a3ddf8a319bfdbbd474fcfdc879/cython-3.2.8-cp39-abi3-win32.whl", hash = "sha256:3fd6464433d925cba66ae31bf5780c8a469a06da1d109180cffb39ee3c88ae20", size = 2435866, upload-time = "2026-06-30T07:43:09.367Z" }, + { url = "https://files.pythonhosted.org/packages/18/4f/911b2b2a0a02be15829ccbf0c906029a318efbf53d9f8e021e438261c206/cython-3.2.8-cp39-abi3-win_arm64.whl", hash = "sha256:4e9447d9b652396a285cdfbd4f9f0721842c63c6df281720e87dcc4b9ea65af5", size = 2457829, upload-time = "2026-06-30T07:43:11.645Z" }, + { url = "https://files.pythonhosted.org/packages/c4/19/31aa63ab719b2e1eea5f200a8a54e2591dc1966a6b75e3de30ef8be9bc2c/cython-3.2.8-py3-none-any.whl", hash = "sha256:f635e113677666de13a2ec2979e9b1d5b90617cdfd1a691d3559be81e2dd6cb9", size = 1258688, upload-time = "2026-06-30T07:41:55.624Z" }, ] [[package]] name = "deepmerge" -version = "2.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890, upload-time = "2024-08-30T05:31:50.308Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/78/6e9e20106224083cfb817d2d3c26e80e72258d617b616721a169b87081e0/deepmerge-2.1.0.tar.gz", hash = "sha256:07ca7a7b8935df596c512fa8161877c0487ac61f691c07766e7d71d2b23bdd2f", size = 21449, upload-time = "2026-06-22T05:46:07.669Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" }, + { url = "https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75", size = 14345, upload-time = "2026-06-22T05:46:06.742Z" }, ] -[[package]] -name = "dnspython" -version = "2.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, -] - -[[package]] -name = "eigen" -version = "3.4.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=release-eigen#f855b17636a376d8399f3df03629ff3e535f6b22" } - [[package]] name = "execnet" version = "2.1.2" @@ -392,11 +395,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] -[[package]] -name = "ffmpeg" -version = "7.1.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=release-ffmpeg#f5153626027621f8ac0cdec056d254d3a1ef6acd" } - [[package]] name = "fonttools" version = "4.63.0" @@ -414,54 +412,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, ] -[[package]] -name = "frozenlist" -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/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" }, - { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, - { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, - { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, - { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, - { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, - { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, - { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, - { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, - { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, - { 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 = "gcc-arm-none-eabi" -version = "13.2.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=release-gcc-arm-none-eabi#15791161bd7a752ef821ad6d999a364b13d6e9ca" } - -[[package]] -name = "git-lfs" -version = "3.6.1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=release-git-lfs#035fb711541a6d8609bb57b26c2f31813c743eb3" } - -[[package]] -name = "google-crc32c" -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/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" }, -] - [[package]] name = "hypothesis" version = "6.47.5" @@ -477,27 +427,22 @@ wheels = [ [[package]] name = "idna" -version = "3.17" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f", size = 196048, upload-time = "2026-05-28T14:32:38.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] -name = "ifaddr" -version = "0.2.0" +name = "importlib-resources" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485, upload-time = "2022-06-15T21:40:27.561Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/06/b56dfa750b44e86157093bc8fca0ab81dccbf5260510de4eaf1cb69b5b99/importlib_resources-7.1.0.tar.gz", hash = "sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708", size = 44985, upload-time = "2026-04-12T16:36:09.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1", size = 37232, upload-time = "2026-04-12T16:36:08.219Z" }, ] -[[package]] -name = "imgui" -version = "1.92.7" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=imgui&rev=release-imgui#af4a30c3930c5e19cf8cea128e342686ba283624" } - [[package]] name = "iniconfig" version = "2.3.0" @@ -537,20 +482,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "json-rpc" -version = "1.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/9e/59f4a5b7855ced7346ebf40a2e9a8942863f644378d956f68bcef2c88b90/json-rpc-1.15.0.tar.gz", hash = "sha256:e6441d56c1dcd54241c937d0a2dcd193bdf0bdc539b5316524713f554b7f85b9", size = 28854, upload-time = "2023-06-11T09:45:49.078Z" } -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 = "json11" -version = "20170411.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=json11&rev=release-json11#0443a506c32eadfec2071a7c163c38c6c7b81d66" } - [[package]] name = "kiwisolver" version = "1.5.0" @@ -579,14 +510,35 @@ wheels = [ ] [[package]] -name = "libjpeg" -version = "3.1.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=release-libjpeg#a615ec26d971cc4cca9a1a426e2c996760fe3c7f" } +name = "libdatachannel-py" +version = "2026.1.0.dev2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2f/68e8306327ddef4b2133d2efb163cb05b319759ce8bd50b8b32dcd03dd95/libdatachannel_py-2026.1.0.dev2-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6607fa1439e1b5bfceecd387c433470c9d45e439c3c06fa064f5c4669ad7e582", size = 1213155, upload-time = "2026-05-19T03:37:12.796Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e3/10aed36ffaf1744795322aae612db777991575b72a9f04e2c677c2c022bf/libdatachannel_py-2026.1.0.dev2-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:a060b1250f57d1fccb36e3a6b36ac8f4fd34926a6b51c564e787e8b7206458aa", size = 1224706, upload-time = "2026-05-19T03:37:12.679Z" }, + { url = "https://files.pythonhosted.org/packages/09/a9/103fc647a8f9c721ab140fe8d2f8dbd90817e917ca763c4eb0f843fe247e/libdatachannel_py-2026.1.0.dev2-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:0b8aa3be2fa3654ea24f882756d6599e847de866a44f7a291c60994548a2debb", size = 1638879, upload-time = "2026-05-19T03:37:18.138Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/0dc7d3fe80fc247ec165dbd455bc2a1a307ef65702a43e24473202c2bf42/libdatachannel_py-2026.1.0.dev2-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:339a79fcbc8c6caf91c620f6e4a0f1b8ccb6a941a966d10a3135c980ae4651a6", size = 1718006, upload-time = "2026-05-19T03:37:11.891Z" }, + { url = "https://files.pythonhosted.org/packages/6d/86/30904a8753e9db60d8c3cf8efda09585fc68f2004d3d7aa2910c93a8eed5/libdatachannel_py-2026.1.0.dev2-cp312-cp312-manylinux_2_38_aarch64.whl", hash = "sha256:b9f476cb065b50856ab2e53bf774ccca9c6a66454b7ce903aaf6dfcdef2a4482", size = 1643757, upload-time = "2026-05-19T03:37:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/d7/9d/1e10131396d28e84a8088a63c14978cc215f6677dc85acdd96b6068f0664/libdatachannel_py-2026.1.0.dev2-cp312-cp312-manylinux_2_38_x86_64.whl", hash = "sha256:1f31db7347549edcd69fcc1ecb8b31e7183894808ccf9387afc49a4d68debaae", size = 1751748, upload-time = "2026-05-19T03:37:06.98Z" }, +] [[package]] -name = "libusb" -version = "1.0.29" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libusb&rev=release-libusb#23132cff0d629bf69590e23b6a682950433aaec8" } +name = "libusb-package" +version = "1.0.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-resources" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/a8/8b3d5dae7340880d556f9f866874b9674b2e3a22fee1e2b96f1ab0feae36/libusb_package-1.0.30.0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:2b98784bac3bedda7e95bb5039dd0eded84b02f36110e4d5d607375366c61090", size = 69516, upload-time = "2026-06-30T07:41:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/52/6f/26de4e9f858ab50e87931f0be268f3c1bbfce33e8584add60da857632142/libusb_package-1.0.30.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4ad25f8d254bbbdd234446d5580b24d62d59bb0e690d8090457dac347f044437", size = 65700, upload-time = "2026-06-30T07:41:03.557Z" }, + { url = "https://files.pythonhosted.org/packages/11/12/9ba8fa91dc95b1cbfa4a68207d4048b08b900fd3686fa72b99846608de01/libusb_package-1.0.30.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c71b5f8c65b286425c02c7d624eee1fd7f08bfb1dfa492ddc20e27f06fee3829", size = 76166, upload-time = "2026-06-30T07:41:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/a7c83535749332825f02d6693868f8ee9ae99c4104e60a483773bb652c0e/libusb_package-1.0.30.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f502ad5a0527b8c0431de817662325c88a1bba2cc334173665b04ad168d7b6d3", size = 76159, upload-time = "2026-06-30T07:41:05.543Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/a1fb1726fc96a8ee3bd0e04e5e505500f022dd310dd348ecebf5cdae7a60/libusb_package-1.0.30.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7a2a1c82f6ef85d9920cbe2898aa6927d4c487744ef9f20b0d7d6d95705095bb", size = 77162, upload-time = "2026-06-30T07:41:06.581Z" }, + { url = "https://files.pythonhosted.org/packages/7b/03/264cefc51275ecb047194c4973bc40b3b278566a64bbe6e74f86bf1e0afc/libusb_package-1.0.30.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2ee5ef8ac6f08402b4e8334f7404850316bdd26037c9a97f2a7456ee9a5d1550", size = 76676, upload-time = "2026-06-30T07:41:07.505Z" }, + { url = "https://files.pythonhosted.org/packages/e5/95/d166eeefe0d9dd5833d5724a97b023ec380c3a2d364040ec0485fd57703c/libusb_package-1.0.30.0-py3-none-win32.whl", hash = "sha256:79728146e1f01e525786900b2ccd8298a8eca53cd94b37fb0885e56aa6f9d60a", size = 78769, upload-time = "2026-06-30T07:41:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/60/4a/ff49bd77f33af05ca26fee29d601beb1e19ca791bb86efece82a3833885c/libusb_package-1.0.30.0-py3-none-win_amd64.whl", hash = "sha256:90808da724c8939a333d931d0c7226372ca5fefd98e2f2f3ef79fd918aa37522", size = 90711, upload-time = "2026-06-30T07:41:09.318Z" }, +] [[package]] name = "libusb1" @@ -599,11 +551,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/08/02aecf6dad627534a5835244ece14d7f187ef430354d9b8551199934d059/libusb1-3.4.0-py3-none-win_amd64.whl", hash = "sha256:b7dcc1f324a895af6aac708bc5513a17373f97349cc2ab8a277519c788bd18ca", size = 141212, upload-time = "2026-05-16T20:59:17.286Z" }, ] -[[package]] -name = "libyuv" -version = "1922.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libyuv&rev=release-libyuv#70295e7a22d89f9c5bdb61dd93c30f0cbb55d815" } - [[package]] name = "markdown" version = "3.10.2" @@ -634,7 +581,7 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.10.9" +version = "3.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, @@ -647,25 +594,15 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/24/080c99d223d158d3a8902769269ab6da5b50f7a0e6e072513907e02b7a6c/matplotlib-3.11.0.tar.gz", hash = "sha256:68c0c7be01b30dcca3638934f7f591df73401235cbdbf0d1ab1c71e7db7f8b57", size = 33251176, upload-time = "2026-06-12T02:29:15.508Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, - { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, - { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, - { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, - { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, -] - -[[package]] -name = "metadrive-simulator" -version = "0.4.2.3" -source = { git = "https://github.com/commaai/metadrive.git?rev=minimal#2716f55a9c7b928ce957a497a15c2c19840c08bc" } -dependencies = [ - { name = "numpy" }, - { name = "panda3d" }, - { name = "panda3d-gltf" }, + { url = "https://files.pythonhosted.org/packages/da/17/f5276b496c61477a6c4fc5e7401f4bfe1c2e5ef7c6cd67896f2ade3809cb/matplotlib-3.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06b5872e9cf11adc8f589ded3ce11bc3e1061ad498259664fabc1f6615beb918", size = 9449976, upload-time = "2026-06-12T02:27:50.989Z" }, + { url = "https://files.pythonhosted.org/packages/82/34/bdd77418adb2178a1d59f044bd67bfebb115896e91b840b8a197eb3f4f4e/matplotlib-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0515d495124be3124340e59f164d901ed4484e2246a5b74cfa483cac3b80bd97", size = 9279307, upload-time = "2026-06-12T02:27:53.247Z" }, + { url = "https://files.pythonhosted.org/packages/94/95/7f522393c88313336b20d70fc849555757b2e5febc22b83b3a3f0fd4bce9/matplotlib-3.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be5f93a1d21981bfb802ded0d77a0caa92d4342a47d45754fac77e314a506344", size = 10031353, upload-time = "2026-06-12T02:27:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/87/ce/8f25a0e3186aefd61913e7467d1b999465bcd0d0c03ac695c1b26ca559b7/matplotlib-3.11.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41635d7909d19e52e924a521dde6d8f670b0f53ab1d0e8c331fa831554f681d1", size = 10839232, upload-time = "2026-06-12T02:27:57.746Z" }, + { url = "https://files.pythonhosted.org/packages/85/c2/db15da2bbdf9e3ca66df7db8e2c33a1dfed67be24a24d2c878efaaff01d6/matplotlib-3.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94f5000f67ca9faa300863ea17f8bce9175cb67b88bec4bc7780502d53dd7c9e", size = 10923899, upload-time = "2026-06-12T02:28:00.223Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/a58a4443a4d052a4ea77557478336aefc26c7981f6408d37adba763aa758/matplotlib-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac6f1ef39f3d0f9e2463303013094992cdbe0f85f43bc54155bc472b2042768e", size = 9329528, upload-time = "2026-06-12T02:28:02.27Z" }, + { url = "https://files.pythonhosted.org/packages/61/0f/4b669589d47733b97ab9df4b58d6fc1e68acb5ea42a928dc7cbdd6bf5871/matplotlib-3.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:9dd11fb612ce7bc60b1de5b4fc87ff959d22317b5de42aabf392f66f97af22eb", size = 9003413, upload-time = "2026-06-12T02:28:04.49Z" }, ] [[package]] @@ -678,72 +615,83 @@ wheels = [ ] [[package]] -name = "multidict" -version = "6.7.1" -source = { registry = "https://pypi.org/simple" } -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/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" }, -] +name = "msgq" +version = "0.0.1" +source = { editable = "msgq_repo" } -[[package]] -name = "ncurses" -version = "6.5" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=release-ncurses#eae89aae0266b3fbae7b808a3c0c19a97a82a81c" } +[package.metadata] +requires-dist = [ + { name = "catch2", marker = "extra == 'dev'", git = "https://github.com/commaai/dependencies.git?subdirectory=catch2&rev=release-catch2" }, + { name = "codespell", marker = "extra == 'dev'" }, + { name = "cppcheck", marker = "extra == 'dev'" }, + { name = "cpplint", marker = "extra == 'dev'" }, + { name = "cython", marker = "extra == 'dev'" }, + { name = "lefthook", marker = "extra == 'dev'" }, + { name = "parameterized", marker = "extra == 'dev'" }, + { name = "ruff", marker = "extra == 'dev'" }, + { name = "scons", marker = "extra == 'dev'" }, + { name = "setuptools", marker = "extra == 'dev'" }, + { name = "ty", marker = "extra == 'dev'" }, +] +provides-extras = ["dev"] [[package]] name = "numpy" -version = "2.4.6" +version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/05/3d27272d30698dc0ecb7fdfaa41ad70303b444f81722bb99bce1d818638a/numpy-2.5.0.tar.gz", hash = "sha256:5a129578019311b6e56bdd714250f19b518f7dceeeb8d1af5490f4942d3f891c", size = 20652461, upload-time = "2026-06-21T20:57:51.95Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, - { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, - { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, - { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, - { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0a/11486d02add7b1384dff7374d124b1cfbb0ee864dcc9f6a2c0380638cf84/numpy-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:489780423903667933b4ed6197b6ec3b75ea5dd17d1d8f0f38d798feb6921561", size = 16789987, upload-time = "2026-06-21T20:56:16.657Z" }, + { url = "https://files.pythonhosted.org/packages/55/b2/285f48640a181947b4587a3766d21ec1eaa7fea833d4b49957e09da467a2/numpy-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ece55976ced6bca95a03ae2839e2e5ccffe8eb6a3e7022415645eb154a81e4e6", size = 11760322, upload-time = "2026-06-21T20:56:19.813Z" }, + { url = "https://files.pythonhosted.org/packages/dd/67/b032db1eb03ca30d16eda3b0c22aaa615338b9263c2fd559d0f29451aca4/numpy-2.5.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c83b664b0e6eee9594fa920cf0639d8af796606d3fad6cc70180c87e4b97c7be", size = 5319605, upload-time = "2026-06-21T20:56:22.173Z" }, + { url = "https://files.pythonhosted.org/packages/b9/83/03fc7300c7c6b6c84c487b1dc80d322817b95fbd1f4dd57a85e23b7198de/numpy-2.5.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bf80333980bf37f523341ddd72c783f39d6829ec7736b9eb99086388a2d52cc2", size = 6653628, upload-time = "2026-06-21T20:56:23.914Z" }, + { url = "https://files.pythonhosted.org/packages/82/49/2ec21730bc63ccfda829323f7040a8ed4715b3852ce658689cf74ee96a8c/numpy-2.5.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1a4874217b36d5ac8fc876f52e39df56f8182c88463e9e2dceabf7ca8b7efb8", size = 15153691, upload-time = "2026-06-21T20:56:25.631Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/f4a3d0637692c49da8ef99d72d52526f92e0a8d6ac4f0ca9f31441b9d9ea/numpy-2.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaa760137137e8d3c920d27927748215b56014f92667dc9b6c27dfc61249255a", size = 16660066, upload-time = "2026-06-21T20:56:28.009Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2f/c354ec86d1f3f5c19649463b0d39652e160736e5b0a4cd18dff0576715c4/numpy-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7174ce8265fc7f7417d171c9ea8fe905220748893ea67a2a7abe726ec331c4b0", size = 16514638, upload-time = "2026-06-21T20:56:30.26Z" }, + { url = "https://files.pythonhosted.org/packages/06/34/43efdcb319988648580f93c11f1ae82cf7e2faa74925e98e454ae3aa95f8/numpy-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b8c3daaf99de52415d20b42f8e8155c78642cb04207d02f9d317a0dcf1b3fb54", size = 18419647, upload-time = "2026-06-21T20:56:32.41Z" }, + { url = "https://files.pythonhosted.org/packages/71/e2/f5d1676b1d7fb682eb5e9a1641e7ebd2414b3216c370661d1029778908b4/numpy-2.5.0-cp312-cp312-win32.whl", hash = "sha256:6206db0af545d73d068add6d992279145f158428d1da6cc49adc4b630c5d6ee5", size = 6056688, upload-time = "2026-06-21T20:56:34.657Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/48f115d1c58a34032facebcd51fdf2d02df2c51d4a46a81dd1197bb2ea6b/numpy-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:6f2d6873e2940c860a309d21e25b1e69af6aaffdd80aa056b04c16380db1c4f2", size = 12419237, upload-time = "2026-06-21T20:56:36.24Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/2e0882f4044d1b1a1b63e875151fb2393389032022a8b7f5657a7996d3b2/numpy-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:a55e1eb2bca2cfd17a16b213c99dfc8502d47b0d494224d2122277d0400935ca", size = 10339912, upload-time = "2026-06-21T20:56:38.733Z" }, ] [[package]] -name = "opencv-python-headless" -version = "4.13.0.92" -source = { registry = "https://pypi.org/simple" } +name = "opendbc" +version = "0.3.1" +source = { editable = "opendbc_repo" } dependencies = [ { name = "numpy" }, + { name = "pycapnp" }, + { name = "pycryptodome" }, + { name = "tqdm" }, ] -wheels = [ - { 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.metadata] +requires-dist = [ + { name = "cffi", marker = "extra == 'testing'" }, + { name = "codespell", marker = "extra == 'testing'" }, + { name = "cpplint", marker = "extra == 'testing'" }, + { name = "gcovr", marker = "extra == 'testing'" }, + { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, + { name = "inputs", marker = "extra == 'examples'" }, + { name = "jinja2", marker = "extra == 'docs'" }, + { name = "lefthook", marker = "extra == 'testing'" }, + { name = "numpy" }, + { name = "pycapnp" }, + { name = "pycryptodome" }, + { name = "ruff", marker = "extra == 'testing'" }, + { name = "tqdm" }, + { name = "tree-sitter", marker = "extra == 'testing'" }, + { name = "tree-sitter-c", marker = "extra == 'testing'" }, + { name = "ty", marker = "extra == 'testing'" }, + { name = "unittest-parallel", marker = "extra == 'testing'" }, + { name = "zstandard", marker = "extra == 'testing'" }, +] +provides-extras = ["testing", "docs", "examples"] + +[package.metadata.requires-dev] +testing = [ + { name = "comma-car-segments", url = "https://huggingface.co/datasets/commaai/commaCarSegments/resolve/main/dist/comma_car_segments-0.1.0-py3-none-any.whl" }, + { name = "cppcheck", git = "https://github.com/commaai/dependencies.git?subdirectory=cppcheck&rev=release-cppcheck" }, ] [[package]] @@ -751,71 +699,54 @@ name = "openpilot" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "acados" }, - { name = "aiohttp" }, - { name = "aiortc" }, - { name = "av" }, - { name = "bootstrap-icons" }, - { name = "bzip2" }, - { name = "capnproto" }, - { name = "catch2" }, - { name = "cffi" }, - { name = "crcmod-plus" }, + { name = "comma-deps-acados" }, + { name = "comma-deps-capnproto" }, + { name = "comma-deps-catch2" }, + { name = "comma-deps-ffmpeg" }, + { name = "comma-deps-gcc-arm-none-eabi" }, + { name = "comma-deps-git-lfs" }, + { name = "comma-deps-json11" }, + { name = "comma-deps-raylib" }, + { name = "comma-deps-zeromq" }, + { name = "comma-deps-zstd" }, { name = "cython" }, - { name = "eigen" }, - { name = "ffmpeg" }, - { name = "gcc-arm-none-eabi" }, - { name = "git-lfs" }, { name = "inputs" }, { name = "jeepney" }, - { name = "json-rpc" }, - { name = "json11" }, - { name = "libjpeg" }, - { name = "libusb" }, - { name = "libusb1" }, - { name = "libyuv" }, - { name = "ncurses" }, { name = "numpy" }, { name = "pillow" }, - { name = "psutil" }, { name = "pycapnp" }, - { name = "pycryptodome" }, - { name = "pyjwt" }, - { name = "pyserial" }, + { name = "pyjwt", extra = ["crypto"] }, { name = "pyzmq" }, { name = "qrcode" }, - { name = "raylib" }, { name = "requests" }, { name = "scons" }, { name = "sentry-sdk" }, { name = "setproctitle" }, - { name = "setuptools" }, { name = "sounddevice" }, - { name = "spidev", marker = "sys_platform == 'linux'" }, - { name = "sympy" }, { name = "tqdm" }, { name = "websocket-client" }, - { name = "xattr" }, - { name = "xvfb" }, - { name = "zeromq" }, { name = "zstandard" }, - { name = "zstd" }, ] [package.optional-dependencies] dev = [ { name = "matplotlib" }, - { name = "opencv-python-headless" }, ] docs = [ - { name = "jinja2" }, { name = "zensical" }, ] +submodules = [ + { name = "msgq" }, + { name = "opendbc" }, + { name = "pandacan" }, + { name = "rednose" }, + { name = "teleoprtc" }, + { name = "tinygrad" }, +] testing = [ { name = "codespell" }, { name = "coverage" }, { name = "hypothesis" }, - { name = "pre-commit-hooks" }, { name = "pytest" }, { name = "pytest-cpp" }, { name = "pytest-mock" }, @@ -825,52 +756,49 @@ testing = [ { name = "ty" }, ] tools = [ - { name = "imgui" }, - { name = "metadrive-simulator", marker = "platform_machine != 'aarch64'" }, + { name = "comma-deps-bootstrap-icons" }, + { name = "comma-deps-bzip2" }, + { name = "comma-deps-imgui" }, + { name = "comma-deps-libusb" }, + { name = "comma-deps-ncurses" }, +] + +[package.dev-dependencies] +standalone = [ + { name = "openpilot", extra = ["submodules"] }, ] [package.metadata] requires-dist = [ - { name = "acados", git = "https://github.com/commaai/dependencies.git?subdirectory=acados&rev=release-acados" }, - { name = "aiohttp" }, - { name = "aiortc" }, - { name = "av" }, - { name = "bootstrap-icons", git = "https://github.com/commaai/dependencies.git?subdirectory=bootstrap-icons&rev=release-bootstrap-icons" }, - { name = "bzip2", git = "https://github.com/commaai/dependencies.git?subdirectory=bzip2&rev=release-bzip2" }, - { name = "capnproto", git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=release-capnproto" }, - { name = "catch2", git = "https://github.com/commaai/dependencies.git?subdirectory=catch2&rev=release-catch2" }, - { name = "cffi" }, { name = "codespell", marker = "extra == 'testing'" }, + { name = "comma-deps-acados" }, + { name = "comma-deps-bootstrap-icons", marker = "extra == 'tools'" }, + { name = "comma-deps-bzip2", marker = "extra == 'tools'" }, + { name = "comma-deps-capnproto" }, + { name = "comma-deps-catch2" }, + { name = "comma-deps-ffmpeg" }, + { name = "comma-deps-gcc-arm-none-eabi" }, + { name = "comma-deps-git-lfs" }, + { name = "comma-deps-imgui", marker = "extra == 'tools'" }, + { name = "comma-deps-json11" }, + { name = "comma-deps-libusb", marker = "extra == 'tools'" }, + { name = "comma-deps-ncurses", marker = "extra == 'tools'" }, + { name = "comma-deps-raylib" }, + { name = "comma-deps-zeromq" }, + { name = "comma-deps-zstd" }, { name = "coverage", marker = "extra == 'testing'" }, - { name = "crcmod-plus" }, { name = "cython" }, - { name = "eigen", git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=release-eigen" }, - { name = "ffmpeg", git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=release-ffmpeg" }, - { name = "gcc-arm-none-eabi", git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=release-gcc-arm-none-eabi" }, - { name = "git-lfs", git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=release-git-lfs" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, - { name = "imgui", marker = "extra == 'tools'", git = "https://github.com/commaai/dependencies.git?subdirectory=imgui&rev=release-imgui" }, { name = "inputs" }, { name = "jeepney" }, - { name = "jinja2", marker = "extra == 'docs'" }, - { name = "json-rpc" }, - { name = "json11", git = "https://github.com/commaai/dependencies.git?subdirectory=json11&rev=release-json11" }, - { name = "libjpeg", git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=release-libjpeg" }, - { name = "libusb", git = "https://github.com/commaai/dependencies.git?subdirectory=libusb&rev=release-libusb" }, - { name = "libusb1" }, - { name = "libyuv", git = "https://github.com/commaai/dependencies.git?subdirectory=libyuv&rev=release-libyuv" }, { name = "matplotlib", marker = "extra == 'dev'" }, - { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", git = "https://github.com/commaai/metadrive.git?rev=minimal" }, - { name = "ncurses", git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=release-ncurses" }, + { name = "msgq", marker = "extra == 'submodules'", editable = "msgq_repo" }, { name = "numpy", specifier = ">=2.0" }, - { name = "opencv-python-headless", marker = "extra == 'dev'" }, + { name = "opendbc", marker = "extra == 'submodules'", editable = "opendbc_repo" }, + { name = "pandacan", marker = "extra == 'submodules'", editable = "panda" }, { name = "pillow" }, - { name = "pre-commit-hooks", marker = "extra == 'testing'" }, - { name = "psutil" }, { name = "pycapnp", specifier = "==2.1.0" }, - { name = "pycryptodome" }, - { name = "pyjwt" }, - { name = "pyserial" }, + { name = "pyjwt", extras = ["crypto"] }, { name = "pytest", marker = "extra == 'testing'" }, { name = "pytest-cpp", marker = "extra == 'testing'" }, { name = "pytest-mock", marker = "extra == 'testing'" }, @@ -878,27 +806,25 @@ requires-dist = [ { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }, { name = "pyzmq" }, { name = "qrcode" }, - { name = "raylib", git = "https://github.com/commaai/dependencies.git?subdirectory=raylib&rev=release-raylib" }, + { name = "rednose", marker = "extra == 'submodules'", editable = "rednose_repo" }, { name = "requests" }, { name = "ruff", marker = "extra == 'testing'" }, { name = "scons" }, { name = "sentry-sdk" }, { name = "setproctitle" }, - { name = "setuptools" }, { name = "sounddevice" }, - { name = "spidev", marker = "sys_platform == 'linux'" }, - { name = "sympy" }, + { name = "teleoprtc", marker = "extra == 'submodules'", editable = "teleoprtc_repo" }, + { name = "tinygrad", marker = "extra == 'submodules'", editable = "tinygrad_repo" }, { name = "tqdm" }, { name = "ty", marker = "extra == 'testing'" }, { name = "websocket-client" }, - { name = "xattr" }, - { name = "xvfb", git = "https://github.com/commaai/dependencies.git?subdirectory=xvfb&rev=release-xvfb" }, { name = "zensical", marker = "extra == 'docs'" }, - { name = "zeromq", git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=release-zeromq" }, { name = "zstandard" }, - { name = "zstd", git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=release-zstd" }, ] -provides-extras = ["docs", "testing", "dev", "tools"] +provides-extras = ["docs", "testing", "dev", "tools", "submodules"] + +[package.metadata.requires-dev] +standalone = [{ name = "openpilot", extras = ["submodules"] }] [[package]] name = "packaging" @@ -910,61 +836,52 @@ wheels = [ ] [[package]] -name = "panda3d" -version = "1.10.14" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { 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" }, - { url = "https://files.pythonhosted.org/packages/11/69/806dcdbaee3e8deee1956abeea0d3d3e504315d2e9814de82a44809a8617/panda3d-1.10.14-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:3c4399a286a142de7ff86f9356d7e526bbbd38892d7f7d39fecb5c33064972bc", size = 55539594, upload-time = "2024-01-08T19:01:15.979Z" }, - { url = "https://files.pythonhosted.org/packages/ad/25/005de5e2b6d0acd546f8b3f2b547cd29e308cdd04a397f0ea68046e26571/panda3d-1.10.14-cp312-cp312-win32.whl", hash = "sha256:e92e0dd907e2af33085a2c31ca2263dc8023b1b7bc70ce1b9fbc84631e130e51", size = 53479813, upload-time = "2024-01-08T19:01:20.923Z" }, - { url = "https://files.pythonhosted.org/packages/74/bb/cb57563855da994614a33f57bd5691fbcd69f12e5ccddd30d387d0be287f/panda3d-1.10.14-cp312-cp312-win_amd64.whl", hash = "sha256:a5f2defd822d38848f8ae1956115adcb6cc7f464f03a67e73681cc72df125ef4", size = 64893222, upload-time = "2024-01-08T19:01:26.347Z" }, +name = "pandacan" +version = "0.0.10" +source = { editable = "panda" } +dependencies = [ + { name = "libusb-package" }, + { name = "libusb1" }, + { name = "opendbc" }, + { name = "spidev", marker = "sys_platform == 'linux'" }, ] -[[package]] -name = "panda3d-gltf" -version = "0.13" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "panda3d" }, - { name = "panda3d-simplepbr" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/07/7f/9f18fc3fa843a080acb891af6bcc12262e7bdf1d194a530f7042bebfc81f/panda3d-gltf-0.13.tar.gz", hash = "sha256:d06d373bdd91cf530909b669f43080e599463bbf6d3ef00c3558bad6c6b19675", size = 25573, upload-time = "2021-05-21T05:46:32.738Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/94/98ed1f81ca0f5daf6de80533805cc1e98ac162abe4e3e1d382caa7ba5c3c/panda3d_gltf-0.13-py3-none-any.whl", hash = "sha256:02d1a980f447bb1895ff4b48c667f289ba78f07a28ef308d8839b665a621efe2", size = 25568, upload-time = "2021-05-21T05:46:31.28Z" }, -] - -[[package]] -name = "panda3d-simplepbr" -version = "0.13.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "panda3d" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/be/c4d1ded04c22b357277cf6e6a44c1ab4abb285a700bd1991460460e05b99/panda3d_simplepbr-0.13.1.tar.gz", hash = "sha256:c83766d7c8f47499f365a07fe1dff078fc8b3054c2689bdc8dceabddfe7f1a35", size = 6216055, upload-time = "2025-03-30T16:57:41.087Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/5d/3744c6550dddf933785a37cdd4a9921fe13284e6d115b5a2637fe390f158/panda3d_simplepbr-0.13.1-py3-none-any.whl", hash = "sha256:cda41cb57cff035b851646956cfbdcc408bee42511dabd4f2d7bd4fbf48c57a9", size = 2457097, upload-time = "2025-03-30T16:57:39.729Z" }, +[package.metadata] +requires-dist = [ + { name = "cffi", marker = "extra == 'dev'" }, + { name = "cppcheck", marker = "extra == 'dev'", git = "https://github.com/commaai/dependencies.git?subdirectory=cppcheck&rev=release-cppcheck" }, + { name = "flaky", marker = "extra == 'dev'" }, + { name = "gcc-arm-none-eabi", marker = "extra == 'dev'", git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=release-gcc-arm-none-eabi" }, + { name = "libusb-package" }, + { name = "libusb1" }, + { name = "opendbc", git = "https://github.com/sunnypilot/opendbc.git?rev=master" }, + { name = "pycryptodome", marker = "extra == 'dev'", specifier = ">=3.9.8" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest-mock", marker = "extra == 'dev'" }, + { name = "pytest-timeout", marker = "extra == 'dev'" }, + { name = "pytest-xdist", marker = "extra == 'dev'" }, + { name = "ruff", marker = "extra == 'dev'" }, + { name = "scons", marker = "extra == 'dev'" }, + { name = "setuptools", marker = "extra == 'dev'" }, + { name = "spidev", marker = "sys_platform == 'linux'" }, ] +provides-extras = ["dev"] [[package]] name = "pillow" -version = "12.2.0" +version = "12.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, - { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, - { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, - { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, - { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, - { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, ] [[package]] @@ -976,60 +893,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "pre-commit-hooks" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ruamel-yaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/36/4d/93e63e48f8fd16d6c1e4cef5dabadcade4d1325c7fd6f29f075a4d2284f3/pre_commit_hooks-6.0.0.tar.gz", hash = "sha256:76d8370c006f5026cdd638a397a678d26dda735a3c88137e05885a020f824034", size = 28293, upload-time = "2025-08-09T19:25:04.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/46/eba9be9daa403fa94854ce16a458c29df9a01c6c047931c3d8be6016cd9a/pre_commit_hooks-6.0.0-py2.py3-none-any.whl", hash = "sha256:76161b76d321d2f8ee2a8e0b84c30ee8443e01376121fd1c90851e33e3bd7ee2", size = 41338, upload-time = "2025-08-09T19:25:03.513Z" }, -] - -[[package]] -name = "propcache" -version = "0.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, - { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, - { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, - { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, - { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, - { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, - { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, - { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, - { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, - { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, - { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, - { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, - { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, -] - -[[package]] -name = "psutil" -version = "7.2.2" -source = { registry = "https://pypi.org/simple" } -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/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]] name = "pycapnp" version = "2.1.0" @@ -1078,18 +941,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, ] -[[package]] -name = "pyee" -version = "13.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -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/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]] name = "pygments" version = "2.20.0" @@ -1108,52 +959,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, ] -[[package]] -name = "pylibsrtp" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/a6/6e532bec974aaecbf9fe4e12538489fb1c28456e65088a50f305aeab9f89/pylibsrtp-1.0.0.tar.gz", hash = "sha256:b39dff075b263a8ded5377f2490c60d2af452c9f06c4d061c7a2b640612b34d4", size = 10858, upload-time = "2025-10-13T16:12:31.552Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/af/89e61a62fa3567f1b7883feb4d19e19564066c2fcd41c37e08d317b51881/pylibsrtp-1.0.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:822c30ea9e759b333dc1f56ceac778707c51546e97eb874de98d7d378c000122", size = 1865017, upload-time = "2025-10-13T16:12:15.62Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0e/8d215484a9877adcf2459a8b28165fc89668b034565277fd55d666edd247/pylibsrtp-1.0.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:aaad74e5c8cbc1c32056c3767fea494c1e62b3aea2c908eda2a1051389fdad76", size = 2182739, upload-time = "2025-10-13T16:12:17.121Z" }, - { url = "https://files.pythonhosted.org/packages/57/3f/76a841978877ae13eac0d4af412c13bbd5d83b3df2c1f5f2175f2e0f68e5/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9209b86e662ebbd17c8a9e8549ba57eca92a3e87fb5ba8c0e27b8c43cd08a767", size = 2732922, upload-time = "2025-10-13T16:12:18.348Z" }, - { url = "https://files.pythonhosted.org/packages/0e/14/cf5d2a98a66fdfe258f6b036cda570f704a644fa861d7883a34bc359501e/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:293c9f2ac21a2bd689c477603a1aa235d85cf252160e6715f0101e42a43cbedc", size = 2434534, upload-time = "2025-10-13T16:12:20.074Z" }, - { url = "https://files.pythonhosted.org/packages/bd/08/a3f6e86c04562f7dce6717cd2206a0f84ca85c5e38121d998e0e330194c3/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_28_i686.whl", hash = "sha256:81fb8879c2e522021a7cbd3f4bda1b37c192e1af939dfda3ff95b4723b329663", size = 2345818, upload-time = "2025-10-13T16:12:21.439Z" }, - { url = "https://files.pythonhosted.org/packages/8e/d5/130c2b5b4b51df5631684069c6f0a6761c59d096a33d21503ac207cf0e47/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4ddb562e443cf2e557ea2dfaeef0d7e6b90e96dd38eb079b4ab2c8e34a79f50b", size = 2774490, upload-time = "2025-10-13T16:12:22.659Z" }, - { url = "https://files.pythonhosted.org/packages/91/e3/715a453bfee3bea92a243888ad359094a7727cc6d393f21281320fe7798c/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:f02e616c9dfab2b03b32d8cc7b748f9d91814c0211086f987629a60f05f6e2cc", size = 2372603, upload-time = "2025-10-13T16:12:24.036Z" }, - { url = "https://files.pythonhosted.org/packages/e3/56/52fa74294254e1f53a4ff170ee2006e57886cf4bb3db46a02b4f09e1d99f/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c134fa09e7b80a5b7fed626230c5bc257fd771bd6978e754343e7a61d96bc7e6", size = 2451269, upload-time = "2025-10-13T16:12:25.475Z" }, - { url = "https://files.pythonhosted.org/packages/1e/51/2e9b34f484cbdd3bac999bf1f48b696d7389433e900639089e8fc4e0da0d/pylibsrtp-1.0.0-cp310-abi3-win32.whl", hash = "sha256:bae377c3b402b17b9bbfbfe2534c2edba17aa13bea4c64ce440caacbe0858b55", size = 1247503, upload-time = "2025-10-13T16:12:27.39Z" }, - { url = "https://files.pythonhosted.org/packages/c3/70/43db21af194580aba2d9a6d4c7bd8c1a6e887fa52cd810b88f89096ecad2/pylibsrtp-1.0.0-cp310-abi3-win_amd64.whl", hash = "sha256:8d6527c4a78a39a8d397f8862a8b7cdad4701ee866faf9de4ab8c70be61fd34d", size = 1601659, upload-time = "2025-10-13T16:12:29.037Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ec/6e02b2561d056ea5b33046e3cad21238e6a9097b97d6ccc0fbe52b50c858/pylibsrtp-1.0.0-cp310-abi3-win_arm64.whl", hash = "sha256:2696bdb2180d53ac55d0eb7b58048a2aa30cd4836dd2ca683669889137a94d2a", size = 1159246, upload-time = "2025-10-13T16:12:30.285Z" }, +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, ] [[package]] name = "pymdown-extensions" -version = "10.21.3" +version = "11.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, -] - -[[package]] -name = "pyopenssl" -version = "26.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" }, + { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, ] [[package]] @@ -1165,18 +986,9 @@ wheels = [ { 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]] -name = "pyserial" -version = "3.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" }, -] - [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1185,9 +997,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -1229,7 +1041,7 @@ wheels = [ [[package]] name = "pytest-xdist" -version = "3.7.1.dev24+g2b4372bd6" +version = "3.7.1.dev24+g2b4372b" source = { git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da#2b4372bd62699fb412c4fe2f95bf9f01bd2018da" } dependencies = [ { name = "execnet" }, @@ -1300,13 +1112,34 @@ wheels = [ ] [[package]] -name = "raylib" -version = "6.0.0.0" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=raylib&rev=release-raylib#dbf05cd774c2f74a70a0ee9ebb80015ed70ce718" } +name = "rednose" +version = "0.0.1" +source = { editable = "rednose_repo" } dependencies = [ { name = "cffi" }, + { name = "comma-deps-eigen" }, + { name = "cython" }, + { name = "numpy" }, + { name = "scons" }, + { name = "setuptools" }, + { name = "sympy" }, ] +[package.metadata] +requires-dist = [ + { name = "cffi" }, + { name = "comma-deps-eigen" }, + { name = "cython" }, + { name = "numpy" }, + { name = "ruff", marker = "extra == 'dev'" }, + { name = "scipy", marker = "extra == 'dev'" }, + { name = "scons" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "ty", marker = "extra == 'dev'" }, +] +provides-extras = ["dev"] + [[package]] name = "requests" version = "2.34.2" @@ -1322,38 +1155,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] -[[package]] -name = "ruamel-yaml" -version = "0.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, -] - [[package]] name = "ruff" -version = "0.15.15" +version = "0.15.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" }, - { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" }, - { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" }, - { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" }, - { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" }, - { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" }, - { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" }, - { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" }, - { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" }, - { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" }, - { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" }, + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, ] [[package]] @@ -1367,15 +1191,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.61.1" +version = "2.64.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/3b/4bc6b348bbd331daa14d4babe9f2b99bc854f4da41560eefb9488d78481d/sentry_sdk-2.61.1.tar.gz", hash = "sha256:9c6adccb3feefa9ba032c8d295ca477575c2f11896046a2b0ad686c47c4af555", size = 459429, upload-time = "2026-06-01T07:24:18.875Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/31/b7341f156a5f6f36f0b4845d6f1c28a2ae4799171dba7007f3a1e9b234b4/sentry_sdk-2.64.0.tar.gz", hash = "sha256:68be2c29e14ae310f8a39e1a79916b6d85c6cb41dcce789d14ff05fe293e4c55", size = 921020, upload-time = "2026-06-30T08:13:47.682Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/54/c9218db183846e08efaf68534889ef42e499dde432778881104a42f7071b/sentry_sdk-2.61.1-py3-none-any.whl", hash = "sha256:fa36eaf4b8ad708f718500d4bdcc1532637526a22beb874d88cbc0a46458b5ae", size = 483735, upload-time = "2026-06-01T07:24:17.027Z" }, + { url = "https://files.pythonhosted.org/packages/36/a8/3fb9a4319efa3b26f5be0e90e6d8918df43fa7c7e977d26390f589501d82/sentry_sdk-2.64.0-py3-none-any.whl", hash = "sha256:715ea91ca860a819e8d8a50a7bde3a80d0df3b4ed7b6660a20fb9a2d084188f1", size = 498901, upload-time = "2026-06-30T08:13:45.566Z" }, ] [[package]] @@ -1457,6 +1281,83 @@ 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 = "teleoprtc" +version = "1.0.1" +source = { editable = "teleoprtc_repo" } +dependencies = [ + { name = "libdatachannel-py" }, +] + +[package.metadata] +requires-dist = [ + { name = "libdatachannel-py", specifier = ">=2026.1.0.dev2" }, + { name = "parameterized", marker = "extra == 'dev'", specifier = ">=0.8" }, + { name = "pre-commit", marker = "extra == 'dev'" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest-asyncio", marker = "extra == 'dev'" }, + { name = "pytest-xdist", marker = "extra == 'dev'" }, +] +provides-extras = ["dev"] + +[[package]] +name = "tinygrad" +version = "0.12.0" +source = { editable = "tinygrad_repo" } + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'docs'" }, + { name = "blobfile", marker = "extra == 'testing'" }, + { name = "boto3", marker = "extra == 'testing'" }, + { name = "bottle", marker = "extra == 'testing'" }, + { name = "capstone", marker = "extra == 'testing-unit'" }, + { name = "gguf", marker = "extra == 'testing-unit'", specifier = ">=0.18" }, + { name = "hypothesis", marker = "extra == 'testing-minimal'", specifier = ">=6.148.9" }, + { name = "influxdb3-python", marker = "extra == 'testing'" }, + { name = "librosa", marker = "extra == 'testing'" }, + { name = "markdown-callouts", marker = "extra == 'docs'" }, + { name = "markdown-exec", extras = ["ansi"], marker = "extra == 'docs'" }, + { name = "mkdocs", marker = "extra == 'docs'" }, + { name = "mkdocs-material", marker = "extra == 'docs'" }, + { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'" }, + { name = "mypy", marker = "extra == 'linting'", specifier = "==1.19.1" }, + { name = "networkx", marker = "extra == 'testing'" }, + { name = "nibabel", marker = "extra == 'testing'" }, + { name = "numba", marker = "extra == 'testing'", specifier = ">=0.55" }, + { name = "numpy", marker = "extra == 'docs'" }, + { name = "numpy", marker = "extra == 'linting'" }, + { name = "numpy", marker = "extra == 'testing-minimal'" }, + { name = "onnx", marker = "extra == 'testing'", specifier = "==1.19.0" }, + { name = "onnx2torch", marker = "extra == 'testing'" }, + { name = "onnxruntime", marker = "extra == 'testing'" }, + { name = "openai", marker = "extra == 'testing-unit'" }, + { name = "opencv-python", marker = "extra == 'testing'" }, + { name = "pandas", marker = "extra == 'testing'" }, + { name = "pillow", marker = "extra == 'testing'" }, + { name = "pre-commit", marker = "extra == 'linting'" }, + { name = "pycocotools", marker = "extra == 'testing'" }, + { name = "pylint", marker = "extra == 'linting'" }, + { name = "pytest", marker = "extra == 'testing-minimal'" }, + { name = "pytest-split", marker = "extra == 'testing-minimal'" }, + { name = "pytest-timeout", marker = "extra == 'testing-minimal'" }, + { name = "pytest-xdist", marker = "extra == 'testing-minimal'" }, + { name = "ruff", marker = "extra == 'linting'", specifier = "==0.14.10" }, + { name = "safetensors", marker = "extra == 'testing-unit'" }, + { name = "sentencepiece", marker = "extra == 'testing'" }, + { name = "tabulate", marker = "extra == 'testing-unit'" }, + { name = "tiktoken", marker = "extra == 'testing'" }, + { name = "tinygrad", extras = ["testing-minimal"], marker = "extra == 'testing-unit'" }, + { name = "tinygrad", extras = ["testing-unit"], marker = "extra == 'testing'" }, + { name = "torch", marker = "extra == 'testing-minimal'", specifier = "==2.9.1" }, + { name = "tqdm", marker = "extra == 'testing-unit'" }, + { name = "transformers", marker = "extra == 'testing'" }, + { name = "typeguard", marker = "extra == 'linting'" }, + { name = "typing-extensions", marker = "extra == 'linting'" }, + { name = "z3-solver", marker = "extra == 'testing-minimal'", specifier = "<4.15.4" }, +] +provides-extras = ["linting", "testing-minimal", "testing-unit", "testing", "docs"] + [[package]] name = "tomli" version = "2.4.1" @@ -1477,48 +1378,39 @@ wheels = [ [[package]] name = "tqdm" -version = "4.67.3" +version = "4.68.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, ] [[package]] name = "ty" -version = "0.0.41" +version = "0.0.56" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/8b/a64ba465cbc5d1b83c561a498ee5e7729b810606220277cafa93983e6ce1/ty-0.0.41.tar.gz", hash = "sha256:1e8b55bf4729634b2db64a7d9541cd880087cd681e87efc36e6a056cf05fb648", size = 5765398, upload-time = "2026-06-01T11:49:36.815Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/07/fb29aea5235b0aa8ecfc4d1cc6ddf9fba8b863d67d96c6d345694d644c43/ty-0.0.56.tar.gz", hash = "sha256:84d114dc3796361c0fc72945016eabd74d46b9ee64f198cb0e485719704681e5", size = 6050123, upload-time = "2026-07-01T16:44:56.036Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/22/b032e4d0f35a436f60eabaa19c70dbed6f5095eb7e30c53ddff918aafcff/ty-0.0.41-py3-none-linux_armv6l.whl", hash = "sha256:5f61c5c06616129ce31dd74141e18be69f5e4204a9a0f4505688213353475b30", size = 11541803, upload-time = "2026-06-01T11:49:33.837Z" }, - { url = "https://files.pythonhosted.org/packages/46/f8/78f073febd728f19f0fcbe2e320855ed337b871ed775e0abe5c7b25bbc6a/ty-0.0.41-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25d6b4fbf198ae7523b8e4845ec3c7e1b1cd5efa1712febcad3856e70e66632b", size = 11275600, upload-time = "2026-06-01T11:49:59.384Z" }, - { url = "https://files.pythonhosted.org/packages/62/c6/b1b46684514e7372960348c723657f56db56799c36542571b49c77c18c87/ty-0.0.41-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3509788ed3753669cdd28d289f281b834ceee91b4de648fcf0ef60677a75b030", size = 10695726, upload-time = "2026-06-01T11:50:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/36/73/badf085590648a14bfe06571799becefaa2b409c5838c39b198b790c8f58/ty-0.0.41-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8c04f1424c872f1d8d28efbd69fb52728fcc6c37aa2d11675bf16f76d33ae14", size = 11196461, upload-time = "2026-06-01T11:50:07.174Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8f/399ee615282f80fd912b5ce4b3fe31206a6d40283e0700046aebbbb893f2/ty-0.0.41-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e52bfce12f3723c376d690625f5c462fe318c46dbe53b16d36c266388ee04c", size = 11310318, upload-time = "2026-06-01T11:49:50.663Z" }, - { url = "https://files.pythonhosted.org/packages/e8/43/204e8fe9f4f8932dd8e42a23e3aa8a365a93ecc601f67172da1c4cea50c0/ty-0.0.41-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4f1d606602dc0eb895a0943af737d638311c7afd48b414897fd42a05e876a76", size = 11785266, upload-time = "2026-06-01T11:49:48.733Z" }, - { url = "https://files.pythonhosted.org/packages/e3/85/9098e96e40324ebd3797c3cad20ea0d855b56219806605e10b6455cf71b3/ty-0.0.41-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8f3c41f0b6b0525bcc8d76a9a5921cccda11886ae11f73e2d51e20e185bafb3", size = 12328093, upload-time = "2026-06-01T11:49:52.945Z" }, - { url = "https://files.pythonhosted.org/packages/0e/41/b6a4d29591e86a27ab1364ddc70d6cee9a78cff1ede5466f6862f0384a68/ty-0.0.41-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bff8c4468933961e656664c3ef190800e3d50582c248aed41bb8a1578d64864a", size = 11979937, upload-time = "2026-06-01T11:49:29.633Z" }, - { url = "https://files.pythonhosted.org/packages/94/b2/6772c2c24fe412ebd0d57166d36afc176948857f7e90f368d321c561a12e/ty-0.0.41-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83077a5c56770b1dab5b086a908a4e77740fb8f29d862ed05bd4c854549603d", size = 11859185, upload-time = "2026-06-01T11:49:46.474Z" }, - { url = "https://files.pythonhosted.org/packages/75/59/e4a1e0feef9d74308621c3a8b1558db97fa1c022948341d0eed1bdfc59a1/ty-0.0.41-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8067fae5532ad3c48295c7ddbd1657ea3ee7392810c57810c9c98fe83d43d57f", size = 12036836, upload-time = "2026-06-01T11:49:39.182Z" }, - { url = "https://files.pythonhosted.org/packages/31/67/38f5312c74d51a510fb46c55b49880af6ca186cb87618938c67e808a45a5/ty-0.0.41-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:73a245242380bac3fbf3005d085c153923a45de8918001fad475af54c6fd50e4", size = 11179559, upload-time = "2026-06-01T11:50:01.355Z" }, - { url = "https://files.pythonhosted.org/packages/8c/55/f8cb36874dc1b8b26c6ae066f8ebec392197caa22becdc55bc7e53d251a5/ty-0.0.41-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4eb25f5dc398aa0a5fbd90de62d6a0fd3713354e566ba93cc53670fe84067ee1", size = 11347231, upload-time = "2026-06-01T11:49:55.093Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/2d4369d7e8240b2dea1d7a7985709c38c6ccc57bbc8dbbdae3db5ee8d71a/ty-0.0.41-py3-none-musllinux_1_2_i686.whl", hash = "sha256:092c3a1ef9e3a189f71428092e736080ef42bf5c14412aa61bee8d644261797d", size = 11446240, upload-time = "2026-06-01T11:49:31.822Z" }, - { url = "https://files.pythonhosted.org/packages/8b/b0/36f09bfb568bc14f8496f2ace503c63c9152b4b65da4518cd3be0aa85312/ty-0.0.41-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53edeb6e2ac430fe789454471f0e22a2a71581100818271150bd8f89fa468cc7", size = 11947795, upload-time = "2026-06-01T11:49:41.654Z" }, - { url = "https://files.pythonhosted.org/packages/92/33/ffd38f5e3ecb25e1b7aac314d902ceaa93be16f4874dc236f9da79509468/ty-0.0.41-py3-none-win32.whl", hash = "sha256:f66bdf3afb71f11d412a4c704fa9691e0209cbc8268a19489c8960c7b74eac8b", size = 10776684, upload-time = "2026-06-01T11:50:03.309Z" }, - { url = "https://files.pythonhosted.org/packages/ce/35/4c3e821906cfb05a88c232f5c1202be7ed336e05f4cc796213c809ab6177/ty-0.0.41-py3-none-win_amd64.whl", hash = "sha256:c18386e5c3a0c3d118ce58ee0d8a35f8d18d6b1020ca8ec690f6064e6230a79b", size = 11860965, upload-time = "2026-06-01T11:49:57.07Z" }, - { url = "https://files.pythonhosted.org/packages/5e/20/7c587e4c3c6da1757555831cac308c4add7a958f8b1c7f76cb1dd55ceabf/ty-0.0.41-py3-none-win_arm64.whl", hash = "sha256:289eb66ee5c3554d59b0ea69885ddc5fc8051f3228dd9fee2501799db4a07117", size = 11206414, upload-time = "2026-06-01T11:49:44.395Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/bce79e7ca5c1cc529d3e0d37ddd1121aea4b68a4f749974ad1cc77161871/ty-0.0.56-py3-none-linux_armv6l.whl", hash = "sha256:186d4a53e15747c947e1ec3d7eec8e345d8e40a1ca10e634c585db52497e87dd", size = 11643066, upload-time = "2026-07-01T16:44:18.374Z" }, + { url = "https://files.pythonhosted.org/packages/80/d1/22555d8a1d719661f10050f3865d877bbf497da908961c75fe22217dd18a/ty-0.0.56-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aae1a980fd9535da0469b7ba2b2e1b54a907743a5e0f442dd57eee9f5bfd034c", size = 11407487, upload-time = "2026-07-01T16:44:20.956Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2d/b3b7a74ce8bc59ef48843ad80179bb0d9598bbd6cfc0d11d519bdf6b1352/ty-0.0.56-py3-none-macosx_11_0_arm64.whl", hash = "sha256:afd3058c0a6c5f241e814734f133008c93ee805f61c9cf4ce7412b8822b5d9ad", size = 10962270, upload-time = "2026-07-01T16:44:22.959Z" }, + { url = "https://files.pythonhosted.org/packages/64/ac/6c2fd7de0304a8a7218a756af74f7e62a5e8540fdb175e0a869e51042345/ty-0.0.56-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:058b52f7a823ac13aae3cae30809dd6b5145794b64d8478f9ef38c75d79b4483", size = 11471406, upload-time = "2026-07-01T16:44:25.327Z" }, + { url = "https://files.pythonhosted.org/packages/50/b6/11d861156861c03c7726b74558f9a0e0092661aff83a4fda1279df28c425/ty-0.0.56-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c66e00c1522add1f2bbdd2e45828c953b35c306b7bef03ec9169c75a63699a0", size = 11445612, upload-time = "2026-07-01T16:44:27.531Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ba/09df108582090f3c0770ec4bc8675affed60248f6793a78d909be16211d9/ty-0.0.56-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40903d71c669a30691b5a5d5728056c7877a1bd6be4f233a38883a8b28cf34d7", size = 12093889, upload-time = "2026-07-01T16:44:29.548Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f7/dbb4b4ccb69cd64c209ae55b1ab788ace8222c2bc1f6845be9e7cbedbf25/ty-0.0.56-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63fe3947fe0c46c69a7d950e6832ee70a9ec17321fefbff3d2e3c20baf9e5bd0", size = 12666337, upload-time = "2026-07-01T16:44:31.586Z" }, + { url = "https://files.pythonhosted.org/packages/86/e9/73f903fe4a3d9ea02f26f57c1eb07e3b1029ec92b0e8c2364718893440e3/ty-0.0.56-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71a0c1a72f9854532e710e119b6871ffe4542c8a65146f1f65dcd78fecd885b4", size = 12280247, upload-time = "2026-07-01T16:44:33.637Z" }, + { url = "https://files.pythonhosted.org/packages/d6/90/cebd222495832f1a00dcd321ba25f3cab804221a4991b992c2178bec68ee/ty-0.0.56-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70d1665596494e24d8ebd198438872b5a56ec3cae5f2bcf6c673be797acc4e3c", size = 11991107, upload-time = "2026-07-01T16:44:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/b7/07/8f7337a07250f42d975cdb6decf47fc5b421e6c7da5e3e7be1e85f63a7e5/ty-0.0.56-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:778f99e51558afc1dbbe48ee38ab6aae7b31390ed8c1a1ef1499b295e9f1e82f", size = 12298970, upload-time = "2026-07-01T16:44:38.243Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b9/a52cd59034a48f5f18c6b155cc2cc36861d874b6d0af204b12c898024c3d/ty-0.0.56-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:867bc5708e0066bb4ff6c7db524bd5deea2676c62bfe71d3303138b3be850af0", size = 11425683, upload-time = "2026-07-01T16:44:40.473Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2e/48e42d33357d52eefb695c0c3fcfc96879b73668a7447d1d1e0ad774fedc/ty-0.0.56-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6012f4189c928edb330a37deb9930f982380bd4aa7c4b8e0428eec9651c7551", size = 11469258, upload-time = "2026-07-01T16:44:42.513Z" }, + { url = "https://files.pythonhosted.org/packages/d5/01/ad1b4138be1e3fa97863af3925aa2134f17a593240c35dc38c3429fb5ad1/ty-0.0.56-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ee83de1a7ff4cc32837ec06134ce391d441bc5b35ecd8d3cfe053f120f3e4c1", size = 11758736, upload-time = "2026-07-01T16:44:44.567Z" }, + { url = "https://files.pythonhosted.org/packages/09/34/9d81967ff240eaa57e9249728ef7b7790747cf6d3c9a98ec86b2cfdcc8ee/ty-0.0.56-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:62619b3b0e2c6248ef30d3f0e2f2217ae9893040585be07f32324242f197cd6f", size = 12100242, upload-time = "2026-07-01T16:44:46.584Z" }, + { url = "https://files.pythonhosted.org/packages/c3/36/f51d4666d2de6cf33c1f3a1fcc4bb6b70b197dd6ceaa491eef71d78fe8e8/ty-0.0.56-py3-none-win32.whl", hash = "sha256:b30687bb5cd9729d34c889a289edf32770388d9bb05243e534e723fb45e0381b", size = 11093759, upload-time = "2026-07-01T16:44:49.171Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b4/8fb5d4acfa4afb152245b20fa263069a7547bd1f8e4bfca4eda280c897d7/ty-0.0.56-py3-none-win_amd64.whl", hash = "sha256:ad4c8c47b6f4e3f9ed3fc0b1a5d650088d229e17dd8f63c1826d6bbe94cc3235", size = 12100327, upload-time = "2026-07-01T16:44:51.26Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fc/6a183e71edde90d0c35c2303f23f7a45b6891d1a2c45daf7b8f869831e19/ty-0.0.56-py3-none-win_arm64.whl", hash = "sha256:57538f273d444a5f1293fa7860e967178afe3917611fc5eff16b64e1204fe0d6", size = 11538780, upload-time = "2026-07-01T16:44:53.8Z" }, ] [[package]] @@ -1539,63 +1431,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, ] -[[package]] -name = "xattr" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -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/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" }, - { url = "https://files.pythonhosted.org/packages/98/25/25cc7d64f07de644b7e9057842227adf61017e5bcfe59a79df79f768874c/xattr-1.3.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b4345387087fffcd28f709eb45aae113d911e1a1f4f0f70d46b43ba81e69ccdd", size = 38797, upload-time = "2025-10-13T22:16:11.624Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/cc350bcdbed006dfcc6ade0ac817693b8b3d4b2787f20e427fd0697042e4/xattr-1.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe92bb05eb849ab468fe13e942be0f8d7123f15d074f3aba5223fad0c4b484de", size = 38956, upload-time = "2025-10-13T22:16:13.121Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b2/9416317ac89e2ed759a861857cda0d5e284c3691e6f460d36cc2bd5ce4d1/xattr-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c42ef5bdac3febbe28d3db14d3a8a159d84ba5daca2b13deae6f9f1fc0d4092", size = 38214, upload-time = "2025-10-13T22:16:14.389Z" }, - { url = "https://files.pythonhosted.org/packages/38/63/188f7cb41ab35d795558325d5cc8ab552171d5498cfb178fd14409651e18/xattr-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2aaa5d66af6523332189108f34e966ca120ff816dfa077ca34b31e6263f8a236", size = 37754, upload-time = "2025-10-13T22:16:15.306Z" }, -] - -[[package]] -name = "xvfb" -version = "1.20.11.post1" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=xvfb&rev=release-xvfb#f0bb0b0d9154ea7858f3c0333b607ba7d356b571" } - -[[package]] -name = "yarl" -version = "1.24.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, - { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, - { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, - { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, - { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, - { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, - { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, - { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, - { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, - { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, - { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, - { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, - { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, - { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, - { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, -] - [[package]] name = "zensical" -version = "0.0.43" +version = "0.0.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1607,27 +1445,22 @@ dependencies = [ { name = "pyyaml" }, { name = "tomli" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d4/85/ec45162e7824a8f879d887ef0774ee65926bf7d1064e2eebccc7eaee3378/zensical-0.0.43.tar.gz", hash = "sha256:dc2d3804ff562795c1024130e0c3ce79736467930729dda314f096d0e35b98c8", size = 3932396, upload-time = "2026-05-19T09:44:07.418Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/57/c7bbb71f943e1e0ba5ce460f4930ec836ead7286969e7fd742f7a6c049ab/zensical-0.0.46.tar.gz", hash = "sha256:3ec21f4fb1e78cd7c0d6b07ae336b04770e27ba020dabc457b2790e5d34f1978", size = 3973968, upload-time = "2026-06-21T18:52:40.368Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/c2/55e0709607ae41c266987c3b91a1a9702b37fbbef0d07eddfe5e25c2d823/zensical-0.0.43-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:17c335362b6bac3a50178181694a964f6d9f0c516fc532129ba5a0a5c4103fb6", size = 12706531, upload-time = "2026-05-19T09:43:32.729Z" }, - { url = "https://files.pythonhosted.org/packages/2c/64/ce8627bc5ea30556162b29b041fe97d6a6aef2a87b51f12def628e4fa608/zensical-0.0.43-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8fe97f185194215f6193af45a17d2b30ebd72c8113e3650f2d7d6767b9c2206", size = 12563012, upload-time = "2026-05-19T09:43:35.962Z" }, - { url = "https://files.pythonhosted.org/packages/66/d1/533bc9454f0e06b3d9d8bd2e7ac405308c3d4dee6572acab98f0ed6d1c07/zensical-0.0.43-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c4c85978c765b3e7f347e8102dfe1373d4bbe4229d7008b6bdbf352f1fbcd7f", size = 12947599, upload-time = "2026-05-19T09:43:38.754Z" }, - { url = "https://files.pythonhosted.org/packages/75/a0/94f47d6fb592997be7ab9526938c929f0199adf2637c3c2b2b9b2101b28e/zensical-0.0.43-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90d7c06ffd07b2bdf78bef041d541baba8a3ea51fd2dd84dbdbc5b0229076524", size = 12904911, upload-time = "2026-05-19T09:43:42.434Z" }, - { url = "https://files.pythonhosted.org/packages/96/fb/1db3ad9a86ff772f74a8bc60ad5b447aa02a158e70f94adacf50bdd5c40f/zensical-0.0.43-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60022f4a6b95e46ec0023f51052fcd491743b3ebd08c0066b22a5cf1e741fecd", size = 13269386, upload-time = "2026-05-19T09:43:45.387Z" }, - { url = "https://files.pythonhosted.org/packages/31/ee/b24fd0f94885519d851c35615b086d069a1077b0198021a56755395a4633/zensical-0.0.43-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e278eb948a0b7545d50609d713c7c27e366dade4523ff73a311a5d5f136518a", size = 12999364, upload-time = "2026-05-19T09:43:48.549Z" }, - { url = "https://files.pythonhosted.org/packages/28/78/401ccd7afd9d2690f81b5319b7f1eed05108154ce20e4207053914518c1c/zensical-0.0.43-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b85e5ab99fbda13823e67c43a4be6e5ebda6600602969c6575e143f20ac203fd", size = 13124392, upload-time = "2026-05-19T09:43:50.965Z" }, - { url = "https://files.pythonhosted.org/packages/98/b3/9af6eba5826b0ef143fc8308bd1e219e221441e307a958e39f824ba9ab53/zensical-0.0.43-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:751385accc92cccfd4560dabed7c423870686ef6ede244a67e5c96286af25e8f", size = 13177538, upload-time = "2026-05-19T09:43:53.964Z" }, - { url = "https://files.pythonhosted.org/packages/be/6b/cd090bd6659d32692487206469988ee84d41aa6de4cdf9e380f847da90e2/zensical-0.0.43-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:dd3ff5bfa6e65cf3d2550dc639c3da2a3bfa11087b83d57e06623c4c1607d583", size = 13327086, upload-time = "2026-05-19T09:43:56.8Z" }, - { url = "https://files.pythonhosted.org/packages/79/5b/ac2555354b5a53cb9c2c942811905c47be0b9f5603d3c1328ee8564333eb/zensical-0.0.43-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85055a115b12f49c6ab194dcf04f966fc06b690ed6a8ddddd819929fc5f340e6", size = 13284645, upload-time = "2026-05-19T09:43:59.329Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c6/1688ec6e5be15e3ab367d7804753291bfbdff3109b06e20c19ce30a7129c/zensical-0.0.43-cp310-abi3-win32.whl", hash = "sha256:8a75ddd4bb3cd3c4a8e71d2ebae44c5611fd636c1d355c6124dd96e2f9c52838", size = 12256740, upload-time = "2026-05-19T09:44:02.102Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a8/d967e70eac810a7e9eb8c5150d6d02848a1f42260f42977c71debed3cb02/zensical-0.0.43-cp310-abi3-win_amd64.whl", hash = "sha256:03a9d1744a6394ad66c355d6f1de04cfd92efa525b0b94bf6dbf6971c5cd2c6b", size = 12496166, upload-time = "2026-05-19T09:44:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/4c/bd/bbc499ee35ac9ec5459dbfec7bb7231556689e97eaa13a5eddbe1f0443b5/zensical-0.0.46-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d91af81ab058c8693dfd75f2f77b4c73bcba4125681d1d276f38624291820bd2", size = 12796482, upload-time = "2026-06-21T18:52:07.369Z" }, + { url = "https://files.pythonhosted.org/packages/88/1b/7acc273184d59b8e894d15ebe3cf1c5e81b3a822fde1792ea3e33be37a2e/zensical-0.0.46-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d9221264a9a87409900a47e29985607b0c9245dacb89077e87c8e16e31edc167", size = 12660030, upload-time = "2026-06-21T18:52:10.186Z" }, + { url = "https://files.pythonhosted.org/packages/80/df/bd0a68de98a19fc6050c58be11f36d05ea72a213b6a7ff7395d33c793747/zensical-0.0.46-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec43018d5343ca2e1d71aa352eeddd560fef504effd03025840a5a783abefa4f", size = 13057130, upload-time = "2026-06-21T18:52:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/f4/db/e27635f5787a42245f900e658340698a6654e165d466f9a3b640efced2cd/zensical-0.0.46-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26e98fb8ab7ab50cdd20a73e2c7d4d9aae0b46cf2d8691e6bb22f9c261b8a60a", size = 13022345, upload-time = "2026-06-21T18:52:15.84Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9d/6ce2ba11c97154870b458a8dae4637ade93b7097912f0102f5ea7fe8cf5b/zensical-0.0.46-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46fe578f26963f8ee89567983e62737b6fadc9197d4742e1020b522e092d7baa", size = 13377445, upload-time = "2026-06-21T18:52:18.538Z" }, + { url = "https://files.pythonhosted.org/packages/68/06/9930d43cd9d2f899b648d63491007c1b4f9716cf118b0c98e867b933069c/zensical-0.0.46-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aef03fa186a5589148e10b62610500989c6b075a2c08e1554233adbf91b2a3dc", size = 13086749, upload-time = "2026-06-21T18:52:21.452Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ed/2342cf860fbb02314938b0d1f1b02344935801b04d185ff3151ef1812898/zensical-0.0.46-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:bc7446cdf97a8dea390f20ed2bd6b030cddc1bd36a8ce113ea3efef6fa61c573", size = 13231120, upload-time = "2026-06-21T18:52:24.171Z" }, + { url = "https://files.pythonhosted.org/packages/de/b0/d2ece02f63cd767fcf10fd7608dc8e0a995f87dc5261209b1dbc296fd57b/zensical-0.0.46-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:bbee37801f1ed500f158dc0992c569282950f780ae353c37fe6969f99983d701", size = 13295035, upload-time = "2026-06-21T18:52:26.942Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/cb0048a612e63e615399fc507472a557d1c5b7c2f74065c5bf11998fd597/zensical-0.0.46-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:9487c147c9cceb50c04d0ad70b024821a6eab1629dafd70ab6d1e86ec841e623", size = 13437191, upload-time = "2026-06-21T18:52:29.69Z" }, + { url = "https://files.pythonhosted.org/packages/91/16/515f81db8055b109a510063be481e60a657c4fad1a883680b2ee4aa9a424/zensical-0.0.46-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f42a4683c762f026878d19ede4bcf7bfbb84dbecb5ad923949abb77806ed88a5", size = 13369382, upload-time = "2026-06-21T18:52:32.521Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5c/da54ee65b642eb7d88dd4a3db35845d0765915638e05d5d434a10b42f1c3/zensical-0.0.46-cp310-abi3-win32.whl", hash = "sha256:85f018f2a7ee76a83915c87ddb12b58cf343fd6154081d33ac95b6751b011dd7", size = 12354298, upload-time = "2026-06-21T18:52:34.976Z" }, + { url = "https://files.pythonhosted.org/packages/73/26/fc7ef081acbdada8436825221cb728ee84a81d4d78a7bb79aa58bd150d31/zensical-0.0.46-cp310-abi3-win_amd64.whl", hash = "sha256:1543a693a160de60e86ca589592401b584670e7e12c5ae30e3c2ba76786f7ec3", size = 12599687, upload-time = "2026-06-21T18:52:37.913Z" }, ] -[[package]] -name = "zeromq" -version = "4.3.5" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=release-zeromq#40b29e8e88a69802e8afc4f71f5c955bc2d5c74c" } - [[package]] name = "zstandard" version = "0.25.0" @@ -1652,8 +1485,3 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, ] - -[[package]] -name = "zstd" -version = "1.5.6" -source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=release-zstd#13015596466eb68825f109ad0d5aefa6330cc495" }